]> gitweb.fperrin.net Git - GpsPrune.git/blobdiff - src/tim/prune/threedee/TerrainHelper.java
Moved source into separate src directory due to popular request
[GpsPrune.git] / src / tim / prune / threedee / TerrainHelper.java
diff --git a/src/tim/prune/threedee/TerrainHelper.java b/src/tim/prune/threedee/TerrainHelper.java
new file mode 100644 (file)
index 0000000..ffe9db8
--- /dev/null
@@ -0,0 +1,477 @@
+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));
+                       }
+               }
+       }
+}