package net.refractions.linecleaner; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; import java.util.Stack; import java.util.Vector; import org.geotools.data.FeatureStore; import org.geotools.feature.Feature; import org.geotools.filter.Filter; import org.geotools.filter.IllegalFilterException; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.MultiLineString; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.operation.distance.DistanceOp; /** * <p> * Useful functions for working with Geometries. * </p> * @author myronwu * @author rgould */ public class GeometryUtil { /** GeometryUtil PRECISION_TOLERANCE field see crudeEquals */ public static final double PRECISION_TOLERANCE = 0.00001; /** * Test whether two geometries have the same end points irrespective of direction. * @param g * @param h * @return Do g and h have equal end points? */ public static boolean identicalEndPoints(Geometry g, Geometry h) { LineString gline = extractLine(g); LineString hline = extractLine(h); if (gline == null || hline == null) { return false; } return identicalEndPoints(gline, hline); } /** * * @param g * @param geoms * @return Do g's nodes connect with geometries in geoms? */ public static boolean connects(Geometry g, Collection<Geometry> geoms) { LineString gline = extractLine(g); if (gline == null) { return false; } Point start = gline.getStartPoint(); Point end = gline.getEndPoint(); boolean startConnects = false; boolean endConnects = false; for (Geometry h: geoms) { LineString hline = extractLine(h); if (hline == null || hline.equals(gline)) { continue; } Point hstart = hline.getStartPoint(); Point hend = hline.getEndPoint(); if (start.equals(hstart) || start.equals(hend)) { startConnects = true; } if (end.equals(hstart) || end.equals(hend)) { endConnects = true; } if (startConnects && endConnects) { return true; } } return false; } /** * * @param g * @param h * @return Do g and h have an end point in common? */ public static boolean hasCommonEndPoint(Geometry g, Geometry h) { LineString gline = extractLine(g); LineString hline = extractLine(h); if (gline == null || hline == null) { return false; } return hasCommonEndPoint(gline,hline); } /** * * @param m * @param n * @return Do m and n have at least one common end point? */ public static boolean hasCommonEndPoint(LineString m, LineString n) { Point mstart = m.getStartPoint(); Point mend = m.getEndPoint(); Point nstart = n.getStartPoint(); Point nend = n.getEndPoint(); return mstart.equals(nstart) || mstart.equals(nend) || mend.equals(nstart) || mend.equals(nend); } /** * Test whether two linestrings have the same end points irrespective of direction. * @param m * @param n * @return Do m and n have the same end points? */ public static boolean identicalEndPoints(LineString m, LineString n) { return (m.getStartPoint().equals(n.getStartPoint()) && m.getEndPoint().equals(n.getEndPoint())) || (m.getStartPoint().equals(n.getEndPoint()) && m.getEndPoint().equals(n.getStartPoint())); } /** * @param g * @return Is g a single linestring multistring? This occurs in * shapefiles. */ public static boolean oneLineMultiString(Geometry g) { return g instanceof MultiLineString && g.getNumGeometries() == 1; } /** * * @param f * @param g * @return Do f and g have identical geometries? */ public static boolean identicalGeometries(Feature f, Feature g) { return f.getDefaultGeometry().equals(g.getDefaultGeometry()); } /** * This is an interface for a function object that's used as a function argument in * linestring subdivision. */ public interface CoordinateFunction { public void run(Coordinate c); } /** * Subdivide g into intervals of length intervalLength. However, in the interest of * saving memory for long linestrings, run a function f on each subdivision point instead * of returning an array of coordinates as in the function below. * @param g * @param intervalLength * @param f Closure fulfilling CoordinateFunction interface. */ public static void subdivide(LineString g, double intervalLength, CoordinateFunction f) { // the linestring is too short to have any intervals. fallback to nodes if (g.getLength() <= intervalLength) { f.run(g.getStartPoint().getCoordinate()); f.run(g.getEndPoint().getCoordinate()); } else { double intervalOffset = 0; int limit = g.getNumPoints() - 2; for (int i = 0; i <= limit; i++) { Line line = new Line(g.getCoordinateN(i), g.getCoordinateN(i+1)); if (intervalOffset < line.getLength()) { line = new Line(line.along(intervalOffset), line.getEnd()); } else { intervalOffset -= line.getLength(); continue; } if (line.getLength() < intervalLength) { // interval's too long, but there's a vertex in the current line if (intervalOffset < intervalLength) { f.run(line.start); } intervalOffset = intervalLength - line.getLength(); } else if (line.getLength() > intervalLength) { int numPoints = line.subdivide(intervalLength, 0, f); intervalOffset = intervalLength - (line.getLength() - (intervalLength * (numPoints-1))); } else { f.run(line.getStart()); f.run(line.getEnd()); } } } } /** * Subdivide a LineString g into intervals of length intervalLength. * @param g * @param intervalLength * @return Coordinates that form subdivisions of g of length intervalLength. */ public static Coordinate[] subdivide(LineString g, double intervalLength) { Stack<Coordinate> result = new Stack<Coordinate>(); Coordinate[] coordinates = g.getCoordinates(); // the linestring is too short to have any intervals. fallback to nodes if (g.getLength() <= intervalLength) { result.push(g.getStartPoint().getCoordinate()); result.push(g.getEndPoint().getCoordinate()); } else { double intervalOffset = 0; int limit = coordinates.length - 2; for (int i = 0; i <= limit; i++) { Line line = new Line(coordinates[i], coordinates[i+1]); if (intervalOffset < line.getLength()) { line = new Line(line.along(intervalOffset), line.getEnd()); } else { intervalOffset -= line.getLength(); continue; } if (line.getLength() < intervalLength) { // interval's too long, but there's a vertex in the current line if (intervalOffset < intervalLength) { result.push(line.start); } intervalOffset = intervalLength - line.getLength(); } else if (line.getLength() > intervalLength) { Collection<Coordinate> subPoints = line.subdivide(intervalLength, 0); result.addAll(subPoints); intervalOffset = intervalLength - (line.getLength() - (intervalLength * (subPoints.size()-1))); } else { result.push(line.getStart()); result.push(line.getEnd()); } } } return result.toArray(new Coordinate[result.size()]); } /** * Extracts a LineString from a either a linestring, or single-linestring * multistring (common in shapefiles). * @param g * @return The LineString in g */ public static LineString extractLine(Geometry g) { LineString line; if (g instanceof LineString) { line = (LineString)g; } else if (oneLineMultiString(g)) { line = (LineString)((MultiLineString)g).getGeometryN(0); } else { line = null; } return line; } /** * Wraps a linestring in a multilinestring. Useful for writing out * to shapefiles where such a thing is expected. * @param line * @return A multilinestring that wraps the given line. */ public static MultiLineString wrapInMultiLineString(LineString line) { GeometryFactory gf = line.getFactory(); return gf.createMultiLineString(new LineString[] {line}); } /** * * @param ls * @param points * @return LineStrings that result from subdividing ls at points. */ public static Collection<LineString> subdivide(LineString ls, Collection<Coordinate> points){ LineString newLineString = addVertices(ls, points); return makeLines(ls.getFactory(), divideIntoCoordinateVectors(newLineString, points)); } protected static Vector<LineString> makeLines(GeometryFactory gf, Vector<Vector<Coordinate>> coordinateVectors) { Vector<LineString> lines = new Vector<LineString>(coordinateVectors.size()); for (Vector<Coordinate> coordinates: coordinateVectors) { lines.add(gf.createLineString(coordinates.toArray(new Coordinate[coordinates.size()]))); } return lines; } protected static Vector<Vector<Coordinate>> divideIntoCoordinateVectors(LineString ls, Collection<Coordinate> points) { Collection<Coordinate> pointsCopy = new HashSet<Coordinate>(points); Vector<Vector<Coordinate>> result = new Vector<Vector<Coordinate>>(pointsCopy.size()+1); result.add(new Vector<Coordinate>()); Coordinate[] coords = ls.getCoordinates(); for (int i = 0, j = 0; i < coords.length; i++) { Coordinate c = coords[i]; if (i != 0 && i != coords.length-1 && pointsCopy.contains(c)) { pointsCopy.remove(c); result.get(j).add(c); // add the current point as end node to the current line // move to the next line j++; Vector<Coordinate> nextPoints = new Vector<Coordinate>(); nextPoints.add(c); result.add(nextPoints); } else { result.get(j).add(c); } } return result; } /** * * @param ls * @param vertices Vertices to add to ls. * @return ls with vertices added in. */ public static LineString addVertices(LineString ls, Collection<Coordinate> vertices) { LineString result = ls; for (Coordinate vertex: vertices) { // this is ugly--is there a way to write a destructive addVertex? result = addVertex(result, vertex); } return result; } /** * Add a vertex to a LineString. It's up to the caller to call this * with a vertex that's actually on the LineString, otherwise the * vertex will be appended as the last coordinate in the LineString. * @param ls * @param vertex * @return ls with vertex added in. */ public static LineString addVertex(LineString ls, Coordinate vertex) { if (ls.isCoordinate(vertex)) { return ls; } Coordinate[] coords = ls.getCoordinates(); int index = findInsertionIndex(ls, vertex); Coordinate[] newCoords = insertCoordinate(coords, vertex, index); return ls.getFactory().createLineString(newCoords); } /** * * @param coords * @param c * @param index * @return Coordinate array with c inserted at index. */ public static Coordinate[] insertCoordinate(Coordinate[] coords, Coordinate c, int index) { Vector<Coordinate> vector = new Vector<Coordinate>(Arrays.asList(coords)); vector.insertElementAt(c, index); return vector.toArray(new Coordinate[coords.length+1]); } /** * Find the position in which to insert a vertex into a LineString's * coordinate array. It's up to the caller to call this with a vertex * on the LineString, otherwise the returned index will be the end * of the array. * @param ls * @param vertex * @return Position in ls's coordinate array in which to insert vertex. */ public static int findInsertionIndex(LineString ls, Coordinate vertex) { Collection<LineString> lines = foldCoordinates(ls); int i = 0; for (LineString line: lines) { // Use a crude intersect here instead of line.intersects(). We're // calling this method with approximations of projected points on the line, // which means line.intersects() never returns true. What we're doing // instead is projecting a second point and testing for rough equality. if (!line.isCoordinate(vertex) && crudeIntersects(line, vertex)) { break; } i++; } return i+1; } protected static boolean crudeIntersects(LineString ls, Coordinate c) { return crudeEquals(GeometryUtil.getClosestPoint(c, ls), c); } protected static boolean crudeEquals(Coordinate c, Coordinate d) { return Math.max(c.x, d.x) - Math.min(c.x,d.x) < PRECISION_TOLERANCE && Math.max(c.y, d.y) - Math.min(c.y, d.y) < PRECISION_TOLERANCE; } /** * Build the collection of lines (represented as one-line linestrings) that make * up the LineString ls. * @param ls * @return Collection of LineStrings made by folding together the coordinates of ls. */ public static Collection<LineString> foldCoordinates(LineString ls) { GeometryFactory gf = ls.getFactory(); Coordinate[] coords = ls.getCoordinates(); Collection<LineString> lines = new LinkedList<LineString>(); int limit = coords.length-1; for (int i = 0; i < limit; i++) { Coordinate start = coords[i]; Coordinate end = coords[i+1]; lines.add(makeLine(gf, start, end)); } return lines; } // convenience costructor for making a line masquerading as linestring protected static LineString makeLine(GeometryFactory gf, Coordinate start, Coordinate end) { return gf.createLineString(new Coordinate[] {start, end}); } /** * * @param store * @param endNode * @param distanceTolerance * @return BBoxFilter to fetch features with distanceTolerance of endNode. */ public static Filter getBBoxFilter(FeatureStore store, Point endNode, double distanceTolerance) { Coordinate coord =endNode.getCoordinate(); double minx = coord.x - distanceTolerance; double maxx = coord.x + distanceTolerance; double miny = coord.y - distanceTolerance; double maxy = coord.y + distanceTolerance; Envelope bounds = new Envelope(minx, maxx, miny, maxy); try { return FeatureUtil.withinBbox(store.getSchema().getDefaultGeometry().getName(), bounds); } catch (IllegalFilterException e) { // TODO Auto-generated catch block e.printStackTrace(); } return null; } /** * * @param c * @param g * @return An approximation of the closest point on Geometry g from Coordinate c. */ public static Coordinate getClosestPoint(Coordinate c, Geometry g) { Point p = g.getFactory().createPoint(c); Coordinate coordinate = DistanceOp.closestPoints(g,p)[0]; return coordinate; } }