content: package tim.prune.threedee;
  
  import java.awt.image.BufferedImage;
  import java.io.File;
  import java.io.IOException;
  
  import javax.imageio.ImageIO;
  import javax.vecmath.Point3d;
  import javax.vecmath.TexCoord2f;
  
  import tim.prune.data.Altitude;
  import tim.prune.data.DataPoint;
  import tim.prune.data.DoubleRange;
  import tim.prune.data.Field;
  import tim.prune.data.FieldList;
  import tim.prune.data.Latitude;
  import tim.prune.data.Longitude;
  import tim.prune.data.Track;
  import tim.prune.data.TrackExtents;
  import tim.prune.data.UnitSetLibrary;
  import tim.prune.gui.map.MapUtils;
  
  /**
   * Helper for generating the arrays needed for the 3d terrain
   */
  public class TerrainHelper
  {
  	/** Number of nodes on each side of the square grid */
  	private final int _gridSize;
  
  	/**
  	 * Constructor
  	 * @param inGridSize grid size
  	 */
  	public TerrainHelper(int inGridSize) {
  		_gridSize = inGridSize;
  	}
  
  	/**
  	 * @return grid size
  	 */
  	public int getGridSize() {
  		return _gridSize;
  	}
  
  
  	/**
  	 * Convert the terrain coordinates from raw form to TriangleStripArray form
  	 * (with repeated nodes)
  	 * @param inRawPoints array of raw points as formed from the track
  	 * @return point coordinates as array
  	 */
  	public Point3d[] getTerrainCoordinates(Point3d[] inRawPoints)
  	{
  		final int numNodes = _gridSize * _gridSize;
  		if (_gridSize <= 1 || inRawPoints == null || inRawPoints.length != numNodes) {return null;}
  		// Put these nodes into a new result array (repeating nodes as necessary)
  		final int resultSize = _gridSize * (_gridSize * 2 - 2);
  		Point3d[] result = new Point3d[resultSize];
  		final int numStrips = _gridSize - 1;
  		int resultIndex = 0;
  		for (int strip=0; strip<numStrips; strip++)
  		{
  			for (int col=0; col<_gridSize; col++)
  			{
  				int bottomNodeIndex = strip * _gridSize + col;
  				int topNodeIndex = bottomNodeIndex + _gridSize;
  				result[resultIndex++] = inRawPoints[bottomNodeIndex];
  				result[resultIndex++] = inRawPoints[topNodeIndex];
  			}
  		}
  		return result;
  	}
  
  
  	/**
  	 * Get the texture coordinates as an array
  	 * @return texture coordinates as array
  	 */
  	public TexCoord2f[] getTextureCoordinates()
  	{
  		if (_gridSize <= 1) {return null;}
  		final int numNodes = _gridSize * _gridSize;
  		final float gridStep = 1.0f / (_gridSize - 1);
  		// Build all the required nodes
  		TexCoord2f[] nodes = new TexCoord2f[numNodes];
  		for (int i=0; i<_gridSize; i++)
  		{
  			for (int j=0; j<_gridSize; j++)
  			{
  				nodes[j * _gridSize + i] = new TexCoord2f(gridStep * i, 1.0f - gridStep * j);
  			}
  		}
  		// Now put these nodes into a new result array (repeating nodes as necessary)
  		final int resultSize = _gridSize * (_gridSize * 2 - 2);
  		TexCoord2f[] result = new TexCoord2f[resultSize];
  		final int numStrips = _gridSize - 1;
  		int resultIndex = 0;
  		for (int strip=0; strip<numStrips; strip++)
  		{
  			for (int col=0; col<_gridSize; col++)
  			{
  				int bottomNodeIndex = strip * _gridSize + col;
  				int topNodeIndex = bottomNodeIndex + _gridSize;
  				result[resultIndex++] = nodes[bottomNodeIndex];
  				result[resultIndex++] = nodes[topNodeIndex];
  			}
  		}
  		return result;
  	}
  
  	/**
  	 * @return strip lengths as array
  	 */
  	public int[] getStripLengths()
  	{
  		final int numStrips = _gridSize - 1;
  		final int nodesPerStrip = _gridSize * 2;
  		int[] result = new int[numStrips];
  		for (int i=0; i<numStrips; i++) {
  			result[i] = nodesPerStrip;
  		}
  		return result;
  	}
  
  	/**
  	 * Create a grid of points in a new Track
  	 * @param inDataTrack track from which the extents should be obtained
  	 * @return Track containing all the points in the grid
  	 */
  	public Track createGridTrack(Track inDataTrack)
  	{
  		// Work out the size of the current track
  		TrackExtents extents = new TrackExtents(inDataTrack);
  		extents.applySquareBorder();
  		DoubleRange xRange = extents.getXRange();
  		DoubleRange yRange = extents.getYRange();
  		// Create the array of points
  		final int numPoints = _gridSize * _gridSize;
  		final double xStep = xRange.getRange() / (_gridSize - 1);
  		final double yStep = yRange.getRange() / (_gridSize - 1);
  		DataPoint[] points = new DataPoint[numPoints];
  		for (int i=0; i<_gridSize; i++)
  		{
  			double pY = yRange.getMinimum() + i * yStep;
  			for (int j=0; j<_gridSize; j++)
  			{
  				// Create a new point with the appropriate lat and long, with no altitude
  				double pX = xRange.getMinimum() + j * xStep;
  				DataPoint point = new DataPoint(
  					Latitude.make(MapUtils.getLatitudeFromY(pY)),
  					Longitude.make(MapUtils.getLongitudeFromX(pX)));
  				points[i * _gridSize + j] = point;
  			}
  		}
  		// Put these into a new track
  		Field[] fields = {Field.LATITUDE, Field.LONGITUDE, Field.ALTITUDE};
  		Track grid = new Track(new FieldList(fields), points);
  		return grid;
  	}
  
  	/**
  	 * Write the given terrain track out to an indexed png file
  	 * @param inModel three-d data model with terrain
  	 * @param inPngFile file to write to
  	 */
  	public void writeHeightMap(ThreeDModel inModel, File inPngFile)
  	{
  		BufferedImage image = new BufferedImage(_gridSize, _gridSize, BufferedImage.TYPE_BYTE_INDEXED);
  		for (int y=0; y<_gridSize; y++)
  		{
  			for (int x=0; x<_gridSize; x++)
  			{
  				double heightValue = inModel.getScaledTerrainValue(y * _gridSize + x) * 256;
  				// Need to ask colour model what rgb to use for this index (a little round-the-houses)
  				image.setRGB(x, y, image.getColorModel().getRGB((int) heightValue));
  			}
  		}
  		try
  		{
  			ImageIO.write(image, "PNG", inPngFile);
  		}
  		catch (IOException ioe) {System.err.println(ioe.getClass().getName() + " - " + ioe.getMessage());}
  	}
  
  
  	/**
  	 * Try to fix the voids in the given terrain track by averaging neighbour values where possible
  	 * @param inTerrainTrack terrain track to fix
  	 */
  	public void fixVoids(Track inTerrainTrack)
  	{
  		int numVoids = countVoids(inTerrainTrack);
  		if (numVoids == 0) {
  			return;
  		}
  		// Fix the holes which are surrounded on all four sides by non-holes
  		fixSingleHoles(inTerrainTrack);
  		// Maybe there is something to do in the corners?
  		fixCornersAndEdges(inTerrainTrack);
  		// Now fix the bigger holes, which should fix everything left
  		fixBiggerHoles(inTerrainTrack);
  		// final int numHolesLeft = countVoids(inTerrainTrack);
  		// if (numHolesLeft > 0) {
  		// 	System.out.println("Fixed bigger holes, now num voids = " + countVoids(inTerrainTrack));
  		// }
  	}
  
  	/**
  	 * @param inTerrainTrack terrain track
  	 * @return number of voids (points without altitudes)
  	 */
  	private static int countVoids(Track inTerrainTrack)
  	{
  		int numVoids = 0;
  		if (inTerrainTrack != null)
  		{
  			for (int i=0; i<inTerrainTrack.getNumPoints(); i++) {
  				if (!inTerrainTrack.getPoint(i).hasAltitude()) {
  					numVoids++;
  				}
  			}
  		}
  		return numVoids;
  	}
  
  	/**
  	 * Just deal with single holes surrounded by at least four direct neighbours
  	 * @param inTerrainTrack terrain track to fix
  	 */
  	private void fixSingleHoles(Track inTerrainTrack)
  	{
  		// Holes with neighbours in all directions
  		final int startIndex = 1, endIndex = _gridSize - 2;
  		for (int x = startIndex; x <= endIndex; x++)
  		{
  			for (int y = startIndex; y <= endIndex; y++)
  			{
  				int pIndex = x * _gridSize + y;
  				// Get the point and its neighbours
  				final DataPoint p = inTerrainTrack.getPoint(pIndex);
  				if (!p.hasAltitude())
  				{
  					final DataPoint pl = inTerrainTrack.getPoint(pIndex - 1);
  					final DataPoint pr = inTerrainTrack.getPoint(pIndex + 1);
  					final DataPoint pu = inTerrainTrack.getPoint(pIndex + _gridSize);
  					final DataPoint pd = inTerrainTrack.getPoint(pIndex - _gridSize);
  					// Check if the points are null??
  					if (pl == null || pr == null || pu == null || pd == null)
  					{
  						System.err.println("Woah. Got a null point in fixSingleHoles. x=" + x + ", y=" + y + ", grid=" + _gridSize);
  						System.err.println("index=" + pIndex);
  						if (pl == null) System.err.println("pl is null");
  						if (pr == null) System.err.println("pr is null");
  						if (pu == null) System.err.println("pu is null");
  						if (pd == null) System.err.println("pd is null");
  						continue;
  					}
  					// Check that all the neighbours have altitudes
  					if (pl.hasAltitude() && pr.hasAltitude() && pu.hasAltitude() && pd.hasAltitude())
  					{
  						// Now check the double-neighbours
  						final DataPoint pll = inTerrainTrack.getPoint(pIndex - 2);
  						final DataPoint prr = inTerrainTrack.getPoint(pIndex + 2);
  						final DataPoint puu = inTerrainTrack.getPoint(pIndex + 2 * _gridSize);
  						final DataPoint pdd = inTerrainTrack.getPoint(pIndex - 2 * _gridSize);
  
  						double altitude = 0.0;
  						if (pll != null && pll.hasAltitude() && prr != null && prr.hasAltitude()
  							&& puu != null && puu.hasAltitude() && pdd != null && pdd.hasAltitude())
  						{
  							// Use the double-neighbours too to take into account the gradients
  							altitude = (
  								  pl.getAltitude().getMetricValue() * 1.5
  								- pll.getAltitude().getMetricValue() * 0.5
  								+ pr.getAltitude().getMetricValue() * 1.5
  								- prr.getAltitude().getMetricValue() * 0.5
  								+ pd.getAltitude().getMetricValue() * 1.5
  								- pdd.getAltitude().getMetricValue() * 0.5
  								+ pu.getAltitude().getMetricValue() * 1.5
  								- puu.getAltitude().getMetricValue() * 0.5) / 4.0;
  						}
  						else
  						{
  							// no double-neighbours, just use neighbours
  							altitude = (
  								  pl.getAltitude().getMetricValue()
  								+ pr.getAltitude().getMetricValue()
  								+ pd.getAltitude().getMetricValue()
  								+ pu.getAltitude().getMetricValue()) / 4.0;
  						}
  						// Set this altitude in the point
  						p.setFieldValue(Field.ALTITUDE, "" + altitude, UnitSetLibrary.getMetricUnitSet(), false);
  					}
  				}
  			}
  		}
  	}
  
  	/**
  	 * Try to fix the corners and edges, if they're blank
  	 * @param inTerrainTrack terrain track
  	 */
  	private void fixCornersAndEdges(Track inTerrainTrack)
  	{
  		fixCorner(inTerrainTrack, 0, 1, 1);
  		fixCorner(inTerrainTrack, _gridSize-1, -1, 1);
  		fixCorner(inTerrainTrack, (_gridSize-1)*_gridSize, 1, -1);
  		fixCorner(inTerrainTrack, _gridSize*_gridSize-1, -1, -1);
  		fixEdge(inTerrainTrack, 0, 1);
  		fixEdge(inTerrainTrack, _gridSize-1, _gridSize);
  		fixEdge(inTerrainTrack, (_gridSize-1)*_gridSize, -_gridSize);
  		fixEdge(inTerrainTrack, _gridSize*_gridSize-1, -1);
  	}
  
  	/**
  	 * Fix a single corner by searching along adjacent edges and averaging the nearest neighbours
  	 * @param inTerrainTrack terrain track
  	 * @param inCornerIndex index of corner to fill
  	 * @param inXinc increment in x direction (+1 or -1)
  	 * @param inYinc increment in y direction (+1 or -1)
  	 */
  	private void fixCorner(Track inTerrainTrack, int inCornerIndex, int inXinc, int inYinc)
  	{
  		DataPoint corner = inTerrainTrack.getPoint(inCornerIndex);
  		if (corner == null || corner.hasAltitude()) {return;}
  		// Corner hasn't got an altitude, we'll have to look for it
  		int sIndex1 = inCornerIndex, sIndex2 = inCornerIndex;
  		Altitude alt1 = null, alt2 = null;
  
  		for (int i=1; i<_gridSize && !corner.hasAltitude(); i++)
  		{
  			sIndex1 += inXinc;
  			sIndex2 += (inYinc * _gridSize);
  			// System.out.println("To fill corner " + inCornerIndex + ", looking at indexes " + sIndex1 + " and " + sIndex2);
  			if (alt1 == null)
  			{
  				DataPoint source1 = inTerrainTrack.getPoint(sIndex1);
  				if (source1 != null && source1.hasAltitude()) {alt1 = source1.getAltitude();}
  			}
  			if (alt2 == null)
  			{
  				DataPoint source2 = inTerrainTrack.getPoint(sIndex2);
  				if (source2 != null && source2.hasAltitude()) {alt2 = source2.getAltitude();}
  			}
  			// Can we average these?
  			if (alt1 != null && alt2 != null)
  			{
  				// System.out.println("Averaging values " + alt1.getMetricValue() + " and " + alt2.getMetricValue());
  				int newAltitude = (int) ((alt1.getMetricValue() + alt2.getMetricValue()) / 2.0);
  				corner.setAltitude("" + newAltitude, UnitSetLibrary.UNITS_METRES, false);
  			}
  		}
  	}
  
  	/**
  	 * Fix any holes found in the specified edge
  	 * @param inTerrainTrack terrain track
  	 * @param inCornerIndex index of corner to start from
  	 * @param inInc increment along edge
  	 */
  	private void fixEdge(Track inTerrainTrack, int inCornerIndex, int inInc)
  	{
  		int prevIndexWithAlt = -1;
  		int sIndex = inCornerIndex;
  		if (inTerrainTrack.getPoint(sIndex).hasAltitude()) {prevIndexWithAlt = 0;}
  		for (int i=1; i<_gridSize; i++)
  		{
  			sIndex += inInc;
  			if (inTerrainTrack.getPoint(sIndex).hasAltitude())
  			{
  				if (prevIndexWithAlt >= 0 && prevIndexWithAlt < (i-1))
  				{
  					final int gapLen = i - prevIndexWithAlt;
  					final int cellIndex1 = inCornerIndex + prevIndexWithAlt * inInc;
  					final double alt1 = inTerrainTrack.getPoint(cellIndex1).getAltitude().getMetricValue();
  					final int cellIndex2 = inCornerIndex + i * inInc;
  					final double alt2 = inTerrainTrack.getPoint(cellIndex2).getAltitude().getMetricValue();
  					//System.out.println("Altitude along edge goes from " + alt1 + " (at " + prevIndexWithAlt + ") to " +
  					//		alt2 + " (at " + i + ")");
  					for (int j = 1; j < gapLen; j++)
  					{
  						final double alt = alt1 + (alt2-alt1) * j / gapLen;
  						//System.out.println("Fill in " + (prevIndexWithAlt + j) + "(" + (inCornerIndex + (prevIndexWithAlt + j) * inInc) + ")  with alt " + (int) alt);
  						final DataPoint p = inTerrainTrack.getPoint(inCornerIndex + (prevIndexWithAlt + j) * inInc);
  						p.setAltitude("" + (int) alt, UnitSetLibrary.UNITS_METRES, false);
  					}
  				}
  				prevIndexWithAlt = i;
  			}
  		}
  	}
  
  	/**
  	 * Try to fix bigger holes by interpolating between neighbours
  	 * @param inTerrainTrack terrain track
  	 */
  	private void fixBiggerHoles(Track inTerrainTrack)
  	{
  		TerrainPatch patch = new TerrainPatch(_gridSize);
  		for (int i=0; i<_gridSize; i++)
  		{
  			int prevHoriz = -1, prevVert = -1;
  			for (int j=0; j<_gridSize; j++)
  			{
  				if (inTerrainTrack.getPoint(i * _gridSize + j).hasAltitude())
  				{
  					if (prevHoriz > -1 && prevHoriz != (j-1))
  					{
  						//System.out.println("Found a gap for y=" + i +" between x=" + prevHoriz + " and " + j + " (" + (j-prevHoriz-1) + ")");
  						double startVal = inTerrainTrack.getPoint(i * _gridSize + prevHoriz).getAltitude().getMetricValue();
  						double endVal   = inTerrainTrack.getPoint(i * _gridSize + j).getAltitude().getMetricValue();
  						for (int k=prevHoriz + 1; k< j; k++)
  						{
  							double val = startVal + (k-prevHoriz) * (endVal-startVal) / (j-prevHoriz);
  							patch.addAltitude(i * _gridSize + k, val, k-prevHoriz, j-prevHoriz);
  						}
  					}
  					prevHoriz = j;
  				}
  				if (inTerrainTrack.getPoint(j * _gridSize + i).hasAltitude())
  				{
  					if (prevVert > -1 && prevVert != (j-1))
  					{
  						//System.out.println("Found a gap for x=" + i +" between y=" + prevVert + " and " + j + " (" + (j-prevVert-1) + ")");
  						double startVal = inTerrainTrack.getPoint(prevVert * _gridSize + i).getAltitude().getMetricValue();
  						double endVal   = inTerrainTrack.getPoint(j * _gridSize + i).getAltitude().getMetricValue();
  						for (int k=prevVert + 1; k< j; k++)
  						{
  							double val = startVal + (k-prevVert) * (endVal-startVal) / (j-prevVert);
  							patch.addAltitude(k * _gridSize + i, val, k-prevVert, j-prevVert);
  						}
  					}
  					prevVert = j;
  				}
  			}
  		}
  		// Smooth the patch to reduce the blocky effect from the voids
  		patch.smooth();
  
  		// Now the doubles have been set and averaged, we can set the values in the points
  		for (int i=0; i<inTerrainTrack.getNumPoints(); i++)
  		{
  			DataPoint p = inTerrainTrack.getPoint(i);
  			if (!p.hasAltitude())
  			{
  				final double altitude = patch.getAltitude(i);
  				p.setAltitude("" + altitude, UnitSetLibrary.UNITS_METRES, false);
  			}
  		}
  	}
  }
  