+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.Coordinate;
+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 int _gridSize = 0;
+
+ /**
+ * 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(
+ new Latitude(MapUtils.getLatitudeFromY(pY), Coordinate.FORMAT_DECIMAL_FORCE_POINT),
+ new Longitude(MapUtils.getLongitudeFromX(pX), Coordinate.FORMAT_DECIMAL_FORCE_POINT),
+ null);
+ //System.out.println("Created point at " + point.getLatitude().output(Coordinate.FORMAT_DEG_MIN_SEC)
+ // + ", " + point.getLongitude().output(Coordinate.FORMAT_DEG_MIN_SEC));
+ 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;}
+ //System.out.println("Starting to fix, num voids = " + numVoids);
+ // Fix the holes which are surrounded on all four sides by non-holes
+ fixSingleHoles(inTerrainTrack);
+ //System.out.println("Fixed single holes, now num voids = " + countVoids(inTerrainTrack));
+ // Maybe there is something to do in the corners?
+ fixCornersAndEdges(inTerrainTrack);
+ //System.out.println("Fixed corners, now num voids = " + countVoids(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)
+ {
+ // DEBUG: Show state of voids first
+// final int gridSize = (int) Math.sqrt(inTerrainTrack.getNumPoints());
+// StringBuilder sb = new StringBuilder();
+// for (int i=0; i<inTerrainTrack.getNumPoints(); i++)
+// {
+// if ((i%gridSize) == 0) sb.append('\n');
+// if (inTerrainTrack.getPoint(i).hasAltitude()) {
+// sb.append('A');
+// } else {
+// sb.append(' ');
+// }
+// }
+// System.out.println("Voids:" + sb.toString());
+ // END DEBUG
+
+ 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, false);
+ // force value to metres
+ p.getAltitude().reset(new Altitude((int) altitude, UnitSetLibrary.UNITS_METRES));
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * 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.setFieldValue(Field.ALTITUDE, "" + newAltitude, false);
+ // TODO: Check forcing metres? Is there a nicer way?
+ }
+ }
+ }
+
+ /**
+ * 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.setFieldValue(Field.ALTITUDE, "" + (int) alt, false);
+ // TODO: Check forcing metres?
+ }
+ }
+ 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.setFieldValue(Field.ALTITUDE, "" + altitude, false);
+ p.getAltitude().reset(new Altitude((int) altitude, UnitSetLibrary.UNITS_METRES));
+ }
+ }
+ }
+}