From cc0c3a1a96e9d56af68055c84ca4472057516c27 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 31 Jul 2023 14:44:01 -0700 Subject: [PATCH 001/123] Fix inverted PolygonNodeTopology tests --- .../jts/algorithm/PolygonNodeTopologyTest.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/PolygonNodeTopologyTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/PolygonNodeTopologyTest.java index 7658007186..56d48e823f 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/PolygonNodeTopologyTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/PolygonNodeTopologyTest.java @@ -16,27 +16,27 @@ public static void main(String args[]) { public void testNonCrossing() { checkCrossing("LINESTRING (500 1000, 1000 1000, 1000 1500)", - "LINESTRING (1000 500, 1000 1000, 500 1500)", false); + "LINESTRING (1000 500, 1000 1000, 500 1500)"); } - public void testCrossingQuadrant2() { + public void testNonCrossingQuadrant2() { checkCrossing("LINESTRING (500 1000, 1000 1000, 1000 1500)", - "LINESTRING (300 1200, 1000 1000, 500 1500)"); + "LINESTRING (300 1200, 1000 1000, 500 1500)", false); } - public void testCrossingQuadrant4() { + public void testNonCrossingQuadrant4() { checkCrossing("LINESTRING (500 1000, 1000 1000, 1000 1500)", - "LINESTRING (1000 500, 1000 1000, 1500 1000)"); + "LINESTRING (1000 500, 1000 1000, 1500 1000)", false); } public void testInteriorSegment() { checkInterior("LINESTRING (5 9, 5 5, 9 5)", - "LINESTRING (5 5, 9 9)"); + "LINESTRING (5 5, 0 0)"); } public void testExteriorSegment() { checkExterior("LINESTRING (5 9, 5 5, 9 5)", - "LINESTRING (5 5, 0 0)"); + "LINESTRING (5 5, 9 9)"); } //----------------------------------------------- @@ -48,7 +48,7 @@ private void checkCrossing(String wktA, String wktB, boolean isExpected) { Coordinate[] a = readPts(wktA); Coordinate[] b = readPts(wktB); // assert: a[1] = b[1] - boolean isCrossing = ! PolygonNodeTopology.isCrossing(a[1], a[0], a[2], b[0], b[2]); + boolean isCrossing = PolygonNodeTopology.isCrossing(a[1], a[0], a[2], b[0], b[2]); assertTrue(isCrossing == isExpected); } @@ -64,7 +64,7 @@ private void checkInteriorSegment(String wktA, String wktB, boolean isExpected) Coordinate[] a = readPts(wktA); Coordinate[] b = readPts(wktB); // assert: a[1] = b[1] - boolean isInterior = ! PolygonNodeTopology.isInteriorSegment(a[1], a[0], a[2], b[1]); + boolean isInterior = PolygonNodeTopology.isInteriorSegment(a[1], a[0], a[2], b[1]); assertTrue(isInterior == isExpected); } From b009789e0d3479da518eb98cfb39e5602c760776 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 31 Jul 2023 14:54:53 -0700 Subject: [PATCH 002/123] Refactor PolygonNodeTopology test --- .../jts/algorithm/PolygonNodeTopologyTest.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/PolygonNodeTopologyTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/PolygonNodeTopologyTest.java index 56d48e823f..3677763eaa 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/PolygonNodeTopologyTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/PolygonNodeTopologyTest.java @@ -20,13 +20,13 @@ public void testNonCrossing() { } public void testNonCrossingQuadrant2() { - checkCrossing("LINESTRING (500 1000, 1000 1000, 1000 1500)", - "LINESTRING (300 1200, 1000 1000, 500 1500)", false); + checkNonCrossing("LINESTRING (500 1000, 1000 1000, 1000 1500)", + "LINESTRING (300 1200, 1000 1000, 500 1500)"); } public void testNonCrossingQuadrant4() { - checkCrossing("LINESTRING (500 1000, 1000 1000, 1000 1500)", - "LINESTRING (1000 500, 1000 1000, 1500 1000)", false); + checkNonCrossing("LINESTRING (500 1000, 1000 1000, 1000 1500)", + "LINESTRING (1000 500, 1000 1000, 1500 1000)"); } public void testInteriorSegment() { @@ -44,6 +44,10 @@ private void checkCrossing(String wktA, String wktB) { checkCrossing(wktA, wktB, true); } + private void checkNonCrossing(String wktA, String wktB) { + checkCrossing(wktA, wktB, false); + } + private void checkCrossing(String wktA, String wktB, boolean isExpected) { Coordinate[] a = readPts(wktA); Coordinate[] b = readPts(wktB); From 495dc57f94e96adfd73ac837d1331d3c5098b760 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 9 Aug 2023 10:31:26 -0700 Subject: [PATCH 003/123] Fix VariableBuffer to handle zero distances (#997) --- .../jts/operation/buffer/VariableBuffer.java | 38 ++++++++++++++----- .../operation/buffer/VariableBufferTest.java | 27 ++++++++++++- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/VariableBuffer.java b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/VariableBuffer.java index c15c3394ed..3624295600 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/VariableBuffer.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/VariableBuffer.java @@ -28,8 +28,9 @@ /** * Creates a buffer polygon with a varying buffer distance * at each vertex along a line. + * Vertex distances may be zero. *

- * Only single lines are supported as input, since buffer widths + * Only single linestrings are supported as input, since buffer widths * are typically specified individually for each line. * * @author Martin Davis @@ -221,7 +222,7 @@ public VariableBuffer(Geometry line, double[] distance) { } /** - * Computes the buffer polygon. + * Computes the variable buffer polygon. * * @return a buffer polygon */ @@ -244,7 +245,7 @@ public Geometry getResult() { .createGeometryCollection(GeometryFactory.toGeometryArray(parts)); Geometry buffer = partsGeom.union(); - // ensure an empty polygon is returned if needed + //-- ensure an empty polygon is returned if needed if (buffer.isEmpty()) { return geomFactory.createPolygon(); } @@ -256,15 +257,24 @@ public Geometry getResult() { * with the given endpoints and buffer distances. * The individual segment buffers are unioned * to form the final buffer. + * If one distance is zero, the end cap at that + * segment end is the endpoint of the segment. + * If both distances are zero, no polygon is returned. * * @param p0 the segment start point * @param p1 the segment end point * @param dist0 the buffer distance at the start point * @param dist1 the buffer distance at the end point - * @return the segment buffer. + * @return the segment buffer, or null if void */ private Polygon segmentBuffer(Coordinate p0, Coordinate p1, double dist0, double dist1) { + /** + * Skip polygon if both distances are zero + */ + if (dist0 <= 0 && dist1 <= 0) + return null; + /** * Compute for increasing distance only, so flip if needed */ @@ -293,22 +303,25 @@ private Polygon segmentBuffer(Coordinate p0, Coordinate p1, LineSegment seg = new LineSegment(p0, p1); Coordinate tr0 = seg.reflect(t0); Coordinate tr1 = seg.reflect(t1); + //-- avoid numeric jitter if first distance is zero + if (dist0 == 0) + tr0 = p0.copy(); CoordinateList coords = new CoordinateList(); - coords.add(t0); - coords.add(t1); + coords.add(t0, false); + coords.add(t1, false); // end cap addCap(p1, dist1, t1, tr1, coords); - coords.add(tr1); - coords.add(tr0); + coords.add(tr1, false); + coords.add(tr0, false); // start cap addCap(p0, dist0, tr0, t0, coords); // close - coords.add(t0); + coords.add(t0, false); Coordinate[] pts = coords.toCoordinateArray(); Polygon polygon = geomFactory.createPolygon(pts); @@ -345,6 +358,11 @@ private Polygon circle(Coordinate center, double radius) { * @param coords the coordinate list to add to */ private void addCap(Coordinate p, double r, Coordinate t1, Coordinate t2, CoordinateList coords) { + //-- handle zero-width at vertex + if (r == 0) { + coords.add(p.copy(), false); + return; + } double angStart = Angle.angle(p, t1); double angEnd = Angle.angle(p, t2); @@ -357,7 +375,7 @@ private void addCap(Coordinate p, double r, Coordinate t1, Coordinate t2, Coordi for (int i = indexStart; i > indexEnd; i--) { // use negative increment to create points CW double ang = capAngle(i); - coords.add( projectPolar(p, r, ang) ); + coords.add( projectPolar(p, r, ang), false ); } } diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/VariableBufferTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/VariableBufferTest.java index dc3953f7be..b5e65a94e8 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/VariableBufferTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/VariableBufferTest.java @@ -17,7 +17,8 @@ public class VariableBufferTest extends GeometryTestCase { - private static final double DEFAULT_TOLERANCE = 1.0e-6; + //-- low tolerance reduces expected geometry literal size + private static final double DEFAULT_TOLERANCE = 1.0e-2; public VariableBufferTest(String name) { super(name); @@ -49,7 +50,7 @@ public void testSegmentInverseDist() { public void testSegmentSameDist() { checkBuffer("LINESTRING (100 100, 200 100)", 10, 10, - "POLYGON ((90 100, 90.19214719596769 101.95090322016128, 90.76120467488713 103.8268343236509, 91.68530387697454 105.55570233019603, 92.92893218813452 107.07106781186548, 94.44429766980397 108.31469612302546, 96.1731656763491 109.23879532511287, 98.04909677983872 109.80785280403231, 100 110, 200 110, 200 110, 201.95090322016128 109.80785280403231, 203.8268343236509 109.23879532511287, 205.55570233019603 108.31469612302546, 207.07106781186548 107.07106781186548, 208.31469612302544 105.55570233019603, 209.23879532511287 103.8268343236509, 209.8078528040323 101.95090322016128, 210 100, 209.8078528040323 98.04909677983872, 209.23879532511287 96.1731656763491, 208.31469612302544 94.44429766980397, 207.07106781186548 92.92893218813452, 205.55570233019603 91.68530387697454, 203.8268343236509 90.76120467488713, 201.95090322016128 90.19214719596769, 200 90, 100 90, 100 90, 98.04909677983872 90.19214719596769, 96.1731656763491 90.76120467488714, 94.44429766980397 91.68530387697454, 92.92893218813452 92.92893218813452, 91.68530387697454 94.44429766980397, 90.76120467488713 96.1731656763491, 90.19214719596769 98.04909677983872, 90 100))" + "POLYGON ((90.192 101.951, 90.761 103.827, 91.685 105.556, 92.929 107.071, 94.444 108.315, 96.173 109.239, 98.049 109.808, 100 110, 200 110, 201.951 109.808, 203.827 109.239, 205.556 108.315, 207.071 107.071, 208.315 105.556, 209.239 103.827, 209.808 101.951, 210 100, 209.808 98.049, 209.239 96.173, 208.315 94.444, 207.071 92.929, 205.556 91.685, 203.827 90.761, 201.951 90.192, 200 90, 100 90, 98.049 90.192, 96.173 90.761, 94.444 91.685, 92.929 92.929, 91.685 94.444, 90.761 96.173, 90.192 98.049, 90 100, 90.192 101.951))" ); } @@ -74,6 +75,20 @@ public void testLargeDistance() { ); } + public void testZeroDistanceAtVertex() { + checkBuffer("LINESTRING( 10 10, 20 20, 30 30)", + new double[] { 5, 0, 5 }, + "MULTIPOLYGON (((5.096 10.975, 5.381 11.913, 5.843 12.778, 6.464 13.536, 7.222 14.157, 7.943 14.557, 20 20, 14.557 7.943, 14.157 7.222, 13.536 6.464, 12.778 5.843, 11.913 5.381, 10.975 5.096, 10 5, 9.025 5.096, 8.087 5.381, 7.222 5.843, 6.464 6.464, 5.843 7.222, 5.381 8.087, 5.096 9.025, 5 10, 5.096 10.975)), ((25.443 32.057, 25.843 32.778, 26.464 33.536, 27.222 34.157, 28.087 34.619, 29.025 34.904, 30 35, 30.975 34.904, 31.913 34.619, 32.778 34.157, 33.536 33.536, 34.157 32.778, 34.619 31.913, 34.904 30.975, 35 30, 34.904 29.025, 34.619 28.087, 34.157 27.222, 33.536 26.464, 32.057 25.443, 20 20, 25.443 32.057)))" + ); + } + + public void testZeroDistancesForSegment() { + checkBuffer("LINESTRING( 10 10, 20 20, 30 30, 40 40)", + new double[] { 5, 0, 0, 5 }, + "MULTIPOLYGON (((5.096 10.975, 5.381 11.913, 5.843 12.778, 6.464 13.536, 7.222 14.157, 7.943 14.557, 20 20, 14.557 7.943, 14.157 7.222, 13.536 6.464, 12.778 5.843, 11.913 5.381, 10.975 5.096, 10 5, 9.025 5.096, 8.087 5.381, 7.222 5.843, 6.464 6.464, 5.843 7.222, 5.381 8.087, 5.096 9.025, 5 10, 5.096 10.975)), ((35.443 42.057, 35.843 42.778, 36.464 43.536, 37.222 44.157, 38.087 44.619, 39.025 44.904, 40 45, 40.975 44.904, 41.913 44.619, 42.778 44.157, 43.536 43.536, 44.157 42.778, 44.619 41.913, 44.904 40.975, 45 40, 44.904 39.025, 44.619 38.087, 44.157 37.222, 43.536 36.464, 42.057 35.443, 30 30, 35.443 42.057)))" + ); + } + private void checkBuffer(String wkt, double startDist, double endDist, String wktExpected) { Geometry geom = read(wkt); @@ -82,6 +97,14 @@ private void checkBuffer(String wkt, double startDist, double endDist, checkBuffer(result, wktExpected); } + private void checkBuffer(String wkt, double[] dist, + String wktExpected) { + Geometry geom = read(wkt); + Geometry result = VariableBuffer.buffer(geom, dist); + //System.out.println(result); + checkBuffer(result, wktExpected); + } + private void checkBuffer(Geometry actual, String wktExpected) { Geometry expected = read(wktExpected); checkEqual(expected, actual, DEFAULT_TOLERANCE); From d37686524cb901892f9dcb91cead4b69166ec26d Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 9 Aug 2023 10:32:50 -0700 Subject: [PATCH 004/123] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 794cf66ad6..83b5e2c07e 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -57,6 +57,7 @@ Distributions for older JTS versions can be obtained at the * Fix `Geometry.getCoordinate` to return non-null coordinate for collections with empty first element (#987) * Fix `LargestEmptyCircle` to handle polygonal obstacles (#988) * Make intersection computation more robust (#989) +* Fix `VariableBuffer` to handle zero vertex buffer distances correctly (#997) ### Performance Improvements From 7a9602efcf2ab77e4be4e2b0284c4d8a77691e77 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 29 Aug 2023 16:32:23 -0700 Subject: [PATCH 005/123] Fix IncrementalDelaunayTriangulator to ensure triangulation boundary is convex (#1004) --- .../jtstest/function/TriangleFunctions.java | 9 + .../IncrementalDelaunayTriangulator.java | 113 +++++++++-- .../triangulate/VoronoiDiagramBuilder.java | 22 ++- .../quadedge/QuadEdgeSubdivision.java | 96 +++++++++- .../jts/triangulate/DelaunayTest.java | 114 ++++++++++-- .../jts/triangulate/VoronoiTest.java | 2 +- .../perf/triangulate/DelaunayStressTest.java | 176 ++++++++++++++++++ 7 files changed, 499 insertions(+), 33 deletions(-) create mode 100644 modules/core/src/test/java/test/jts/perf/triangulate/DelaunayStressTest.java diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/TriangleFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/TriangleFunctions.java index 0e64d6e115..f99b1d74f7 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/TriangleFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/TriangleFunctions.java @@ -52,6 +52,15 @@ public static double circumradius(Geometry g) return Triangle.circumradius(pts[0], pts[1], pts[2]); } + public static Geometry circumcircle(Geometry g, int quadSegs) + { + Coordinate[] pts = trianglePts(g); + Coordinate cc = Triangle.circumcentreDD(pts[0], pts[1], pts[2]); + Geometry ccPt = g.getFactory().createPoint(cc); + double cr = Triangle.circumradius(pts[0], pts[1], pts[2]); + return ccPt.buffer(cr, quadSegs); + } + public static Geometry circumcentreDD(Geometry g) { return GeometryMapper.map(g, diff --git a/modules/core/src/main/java/org/locationtech/jts/triangulate/IncrementalDelaunayTriangulator.java b/modules/core/src/main/java/org/locationtech/jts/triangulate/IncrementalDelaunayTriangulator.java index 220a3cb00d..17f794944b 100644 --- a/modules/core/src/main/java/org/locationtech/jts/triangulate/IncrementalDelaunayTriangulator.java +++ b/modules/core/src/main/java/org/locationtech/jts/triangulate/IncrementalDelaunayTriangulator.java @@ -15,6 +15,8 @@ import java.util.Collection; import java.util.Iterator; +import org.locationtech.jts.algorithm.Orientation; +import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.triangulate.quadedge.LocateFailureException; import org.locationtech.jts.triangulate.quadedge.QuadEdge; import org.locationtech.jts.triangulate.quadedge.QuadEdgeSubdivision; @@ -32,6 +34,7 @@ public class IncrementalDelaunayTriangulator { private QuadEdgeSubdivision subdiv; private boolean isUsingTolerance = false; + private boolean isForceConvex = true; /** * Creates a new triangulator using the given {@link QuadEdgeSubdivision}. @@ -46,6 +49,22 @@ public IncrementalDelaunayTriangulator(QuadEdgeSubdivision subdiv) { } + /** + * Sets whether the triangulation is forced to have a convex boundary. Because + * of the use of a finite-size frame, this condition requires special logic to + * enforce. The default is true, since this is a requirement for some uses of + * Delaunay Triangulations (such as Concave Hull generation). However, forcing + * the triangulation boundary to be convex may cause the overall frame + * triangulation to be non-Delaunay. This can cause a problem for Voronoi + * generation, so the logic can be disabled via this method. + * + * @param isForceConvex true if the triangulation boundary is forced to be + * convex + */ + public void forceConvex(boolean isForceConvex) { + this.isForceConvex = isForceConvex; + } + /** * Inserts all sites in a collection. The inserted vertices MUST be * unique up to the provided tolerance value. (i.e. no two vertices should be @@ -105,19 +124,89 @@ else if (subdiv.isOnEdge(e, v.getCoordinate())) { e = base.oPrev(); } while (e.lNext() != startEdge); - // Examine suspect edges to ensure that the Delaunay condition - // is satisfied. + /** + * Examine suspect edges to ensure that the Delaunay condition is satisfied. + * If it is not, flip the edge and continue scanning. + * + * Since the frame is not infinitely far away, + * edges which touch the frame or are adjacent to it require special logic + * to ensure the inner triangulation maintains a convex boundary. + */ do { - QuadEdge t = e.oPrev(); - if (t.dest().rightOf(e) && v.isInCircle(e.orig(), t.dest(), e.dest())) { - QuadEdge.swap(e); - e = e.oPrev(); - } else if (e.oNext() == startEdge) { - return base; // no more suspect edges. - } else { - e = e.oNext().lPrev(); - } - } while (true); + //-- general case - flip if vertex is in circumcircle + QuadEdge t = e.oPrev(); + boolean doFlip = t.dest().rightOf(e) && v.isInCircle(e.orig(), t.dest(), e.dest()); + + if (isForceConvex) { + //-- special cases to ensure triangulation boundary is convex + if (isConcaveBoundary(e)) { + //-- flip if the triangulation boundary is concave + doFlip = true; + } + else if (isBetweenFrameAndInserted(e, v)) { + //-- don't flip if edge lies between the inserted vertex and a frame vertex + doFlip = false; + } + } + + if (doFlip) { + //-- flip the edge within its quadrilateral + QuadEdge.swap(e); + e = e.oPrev(); + continue; + } + + if (e.oNext() == startEdge) { + return base; // no more suspect edges. + } else { + e = e.oNext().lPrev(); + } + } while (true); } + /** + * Tests if a edge touching a frame vertex + * creates a concavity in the triangulation boundary. + * + * @param e the edge to test + * @return true if the triangulation boundary is concave at the edge + */ + private boolean isConcaveBoundary(QuadEdge e) { + if (subdiv.isFrameVertex(e.dest())) { + return isConcaveAtOrigin(e); + } + if (subdiv.isFrameVertex(e.orig())) { + return isConcaveAtOrigin(e.sym()); + } + return false; + } + + /** + * Tests if the quadrilateral surrounding an edge is concave at the edge origin. + * Used to determine if the triangulation boundary has a concavity. + * @param e + * @return + */ + private static boolean isConcaveAtOrigin(QuadEdge e) { + Coordinate p = e.orig().getCoordinate(); + Coordinate pp = e.oPrev().dest().getCoordinate(); + Coordinate pn = e.oNext().dest().getCoordinate(); + boolean isConcave = Orientation.COUNTERCLOCKWISE == Orientation.index(pp, pn, p); + return isConcave; + } + + /** + * Edges whose adjacent triangles contain + * a frame vertex and the inserted vertex must not be flipped. + * + * @param e the edge to test + * @param vInsert the inserted vertex + * @return true if the edge is between the frame and inserted vertex + */ + private boolean isBetweenFrameAndInserted(QuadEdge e, Vertex vInsert) { + Vertex v1 = e.oNext().dest(); + Vertex v2 = e.oPrev().dest(); + return (v1 == vInsert && subdiv.isFrameVertex(v2)) + || (v2 == vInsert && subdiv.isFrameVertex(v1)); + } } diff --git a/modules/core/src/main/java/org/locationtech/jts/triangulate/VoronoiDiagramBuilder.java b/modules/core/src/main/java/org/locationtech/jts/triangulate/VoronoiDiagramBuilder.java index 37dc9d29e5..5064fbdc84 100644 --- a/modules/core/src/main/java/org/locationtech/jts/triangulate/VoronoiDiagramBuilder.java +++ b/modules/core/src/main/java/org/locationtech/jts/triangulate/VoronoiDiagramBuilder.java @@ -125,9 +125,14 @@ private void create() List vertices = DelaunayTriangulationBuilder.toVertices(siteCoords); subdiv = new QuadEdgeSubdivision(diagramEnv, tolerance); IncrementalDelaunayTriangulator triangulator = new IncrementalDelaunayTriangulator(subdiv); + /** + * Avoid creating very narrow triangles along triangulation boundary. + * These otherwise can cause malformed Voronoi cells. + */ + triangulator.forceConvex(false); triangulator.insertSites(vertices); } - + /** * Gets the {@link QuadEdgeSubdivision} which models the computed diagram. * @@ -155,11 +160,20 @@ public Geometry getDiagram(GeometryFactory geomFact) create(); Geometry polys = subdiv.getVoronoiDiagram(geomFact); - // clip polys to diagramEnv + /* + System.out.println(polys); + Geometry tris = subdiv.getTriangles(true, geomFact); + System.out.println(tris); + if (! subdiv.isFrameDelaunay()) { + throw new IllegalStateException("Triangulation frame is not Delaunay"); + } + //*/ + + //-- clip polys to diagramEnv return clipGeometryCollection(polys, diagramEnv); } - - private static Geometry clipGeometryCollection(Geometry geom, Envelope clipEnv) + + private static Geometry clipGeometryCollection(Geometry geom, Envelope clipEnv) { Geometry clipPoly = geom.getFactory().toGeometry(clipEnv); List clipped = new ArrayList(); diff --git a/modules/core/src/main/java/org/locationtech/jts/triangulate/quadedge/QuadEdgeSubdivision.java b/modules/core/src/main/java/org/locationtech/jts/triangulate/quadedge/QuadEdgeSubdivision.java index 2a2e9c57b7..ba98c107b4 100644 --- a/modules/core/src/main/java/org/locationtech/jts/triangulate/quadedge/QuadEdgeSubdivision.java +++ b/modules/core/src/main/java/org/locationtech/jts/triangulate/quadedge/QuadEdgeSubdivision.java @@ -80,7 +80,7 @@ public static void getTriangleEdges(QuadEdge startQE, QuadEdge[] triEdge) { private final static double EDGE_COINCIDENCE_TOL_FACTOR = 1000; - private static final double FRAME_SIZE_FACTOR = 100.0; + private static final double FRAME_SIZE_FACTOR = 10.0; // debugging only - preserve current subdiv statically // private static QuadEdgeSubdivision currentSubdiv; @@ -617,6 +617,24 @@ public List getPrimaryEdges(boolean includeFrame) { return edges; } + /** + * Gets the edges which touch frame vertices. The returned edges are oriented so + * that their origin is a frame vertex. + * + * @return the edges which touch the frame + */ + public List getFrameEdges() { + List edges = getPrimaryEdges(true); + List frameEdges = new ArrayList(); + for (QuadEdge e : edges) { + if (isFrameEdge(e)) { + QuadEdge fe = isFrameVertex(e.orig()) ? e : e.sym(); + frameEdges.add(fe); + } + } + return frameEdges; + } + /** * A TriangleVisitor which computes and sets the * circumcentre as the origin of the dual @@ -867,6 +885,25 @@ public Geometry getTriangles(GeometryFactory geomFact) { return geomFact.createGeometryCollection(tris); } + /** + * Gets the geometry for the triangles in a triangulated subdivision as a {@link GeometryCollection} + * of triangular {@link Polygon}s, optionally including the frame triangles. + * + * @param includeFrame true if the frame triangles should be included + * @param geomFact the GeometryFactory to use + * @return a GeometryCollection of triangular Polygons + */ + public Geometry getTriangles(boolean includeFrame, GeometryFactory geomFact) { + List triPtsList = getTriangleCoordinates(includeFrame); + Polygon[] tris = new Polygon[triPtsList.size()]; + int i = 0; + for (Iterator it = triPtsList.iterator(); it.hasNext();) { + Coordinate[] triPt = (Coordinate[]) it.next(); + tris[i++] = geomFact.createPolygon(geomFact.createLinearRing(triPt)); + } + return geomFact.createGeometryCollection(tris); + } + /** * Gets the cells in the Voronoi diagram for this triangulation. * The cells are returned as a {@link GeometryCollection} of {@link Polygon}s @@ -957,4 +994,61 @@ public Polygon getVoronoiCellPolygon(QuadEdge qe, GeometryFactory geomFact) return cellPoly; } + /** + * Tests whether a subdivision is a valid Delaunay Triangulation. + * This is the case iff every edge is locally Delaunay, meaning that + * the apex of one adjacent triangle is not inside the circumcircle + * of the other adjacent triangle. + * + * @return true if the subdivision is Delaunay + */ + public boolean isDelaunay() { + List edges = getPrimaryEdges(true); + for (QuadEdge e : edges) { + Vertex a0 = e.oPrev().dest(); + Vertex a1 = e.oNext().dest(); + boolean isDelaunay = ! a1.isInCircle(e.orig(), a0, e.dest()); + if (! isDelaunay) { + /* + System.out.println(WKTWriter.toLineString(new Coordinate[] { + e.orig().getCoordinate(), a0.getCoordinate(), e.dest().getCoordinate() + })); + */ + return false; + } + } + return true; + } + + /** + * Tests whether the frame edges are Delaunay + * @return true if the frame edges are Delaunay + */ + /* + public boolean isFrameDelaunay() { + List edges = getFrameEdges(); + for (QuadEdge e : edges) { + Vertex a0 = e.oPrev().dest(); + Vertex a1 = e.oNext().dest(); + boolean isDelaunay = ! a1.isInCircle(e.orig(), a0, e.dest()); + if (! isDelaunay) { + + return false; + } + } + return true; + } + + public void makeFrameDelaunay() { + List edges = getFrameEdges(); + for (QuadEdge e : edges) { + Vertex a0 = e.oPrev().dest(); + Vertex a1 = e.oNext().dest(); + boolean isDelaunay = ! a1.isInCircle(e.orig(), a0, e.dest()); + if (! isDelaunay) { + QuadEdge.swap(e); + } + } + } + */ } diff --git a/modules/core/src/test/java/org/locationtech/jts/triangulate/DelaunayTest.java b/modules/core/src/test/java/org/locationtech/jts/triangulate/DelaunayTest.java index 90dff39cc0..7443ef5061 100644 --- a/modules/core/src/test/java/org/locationtech/jts/triangulate/DelaunayTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/triangulate/DelaunayTest.java @@ -11,19 +11,21 @@ */ package org.locationtech.jts.triangulate; +import org.locationtech.jts.algorithm.ConvexHull; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.io.ParseException; import org.locationtech.jts.io.WKTReader; +import org.locationtech.jts.operation.overlayng.CoverageUnion; -import junit.framework.TestCase; import junit.textui.TestRunner; +import test.jts.GeometryTestCase; /** * Tests Delaunay Triangulation classes * */ -public class DelaunayTest extends TestCase { +public class DelaunayTest extends GeometryTestCase { public static void main(String args[]) { TestRunner.run(DelaunayTest.class); @@ -39,9 +41,9 @@ public void testTriangle() { String wkt = "MULTIPOINT ((10 10 1), (10 20 2), (20 20 3))"; String expected = "MULTILINESTRING ((10 20, 20 20), (10 10, 10 20), (10 10, 20 20))"; - runDelaunayEdges(wkt, expected); + checkDelaunayEdges(wkt, expected); String expectedTri = "GEOMETRYCOLLECTION (POLYGON ((10 20, 10 10, 20 20, 10 20)))"; - runDelaunay(wkt, true, expectedTri); + checkDelaunay(wkt, true, expectedTri); } public void testRandom() @@ -49,9 +51,9 @@ public void testRandom() { String wkt = "MULTIPOINT ((50 40), (140 70), (80 100), (130 140), (30 150), (70 180), (190 110), (120 20))"; String expected = "MULTILINESTRING ((70 180, 190 110), (30 150, 70 180), (30 150, 50 40), (50 40, 120 20), (190 110, 120 20), (120 20, 140 70), (190 110, 140 70), (130 140, 140 70), (130 140, 190 110), (70 180, 130 140), (80 100, 130 140), (70 180, 80 100), (30 150, 80 100), (50 40, 80 100), (80 100, 120 20), (80 100, 140 70))"; - runDelaunayEdges(wkt, expected); + checkDelaunayEdges(wkt, expected); String expectedTri = "GEOMETRYCOLLECTION (POLYGON ((30 150, 50 40, 80 100, 30 150)), POLYGON ((30 150, 80 100, 70 180, 30 150)), POLYGON ((70 180, 80 100, 130 140, 70 180)), POLYGON ((70 180, 130 140, 190 110, 70 180)), POLYGON ((190 110, 130 140, 140 70, 190 110)), POLYGON ((190 110, 140 70, 120 20, 190 110)), POLYGON ((120 20, 140 70, 80 100, 120 20)), POLYGON ((120 20, 80 100, 50 40, 120 20)), POLYGON ((80 100, 140 70, 130 140, 80 100)))"; - runDelaunay(wkt, true, expectedTri); + checkDelaunay(wkt, true, expectedTri); } public void testGrid() @@ -59,9 +61,9 @@ public void testGrid() { String wkt = "MULTIPOINT ((10 10), (10 20), (20 20), (20 10), (20 0), (10 0), (0 0), (0 10), (0 20))"; String expected = "MULTILINESTRING ((10 20, 20 20), (0 20, 10 20), (0 10, 0 20), (0 0, 0 10), (0 0, 10 0), (10 0, 20 0), (20 0, 20 10), (20 10, 20 20), (10 20, 20 10), (10 10, 20 10), (10 10, 10 20), (10 10, 0 20), (10 10, 0 10), (10 0, 10 10), (0 10, 10 0), (10 10, 20 0))"; - runDelaunayEdges(wkt, expected); + checkDelaunayEdges(wkt, expected); String expectedTri = "GEOMETRYCOLLECTION (POLYGON ((0 20, 0 10, 10 10, 0 20)), POLYGON ((0 20, 10 10, 10 20, 0 20)), POLYGON ((10 20, 10 10, 20 10, 10 20)), POLYGON ((10 20, 20 10, 20 20, 10 20)), POLYGON ((10 0, 20 0, 10 10, 10 0)), POLYGON ((10 0, 10 10, 0 10, 10 0)), POLYGON ((10 0, 0 10, 0 0, 10 0)), POLYGON ((10 10, 20 0, 20 10, 10 10)))"; - runDelaunay(wkt, true, expectedTri); + checkDelaunay(wkt, true, expectedTri); } public void testCircle() @@ -69,7 +71,7 @@ public void testCircle() { String wkt = "POLYGON ((42 30, 41.96 29.61, 41.85 29.23, 41.66 28.89, 41.41 28.59, 41.11 28.34, 40.77 28.15, 40.39 28.04, 40 28, 39.61 28.04, 39.23 28.15, 38.89 28.34, 38.59 28.59, 38.34 28.89, 38.15 29.23, 38.04 29.61, 38 30, 38.04 30.39, 38.15 30.77, 38.34 31.11, 38.59 31.41, 38.89 31.66, 39.23 31.85, 39.61 31.96, 40 32, 40.39 31.96, 40.77 31.85, 41.11 31.66, 41.41 31.41, 41.66 31.11, 41.85 30.77, 41.96 30.39, 42 30))"; String expected = "MULTILINESTRING ((41.66 31.11, 41.85 30.77), (41.41 31.41, 41.66 31.11), (41.11 31.66, 41.41 31.41), (40.77 31.85, 41.11 31.66), (40.39 31.96, 40.77 31.85), (40 32, 40.39 31.96), (39.61 31.96, 40 32), (39.23 31.85, 39.61 31.96), (38.89 31.66, 39.23 31.85), (38.59 31.41, 38.89 31.66), (38.34 31.11, 38.59 31.41), (38.15 30.77, 38.34 31.11), (38.04 30.39, 38.15 30.77), (38 30, 38.04 30.39), (38 30, 38.04 29.61), (38.04 29.61, 38.15 29.23), (38.15 29.23, 38.34 28.89), (38.34 28.89, 38.59 28.59), (38.59 28.59, 38.89 28.34), (38.89 28.34, 39.23 28.15), (39.23 28.15, 39.61 28.04), (39.61 28.04, 40 28), (40 28, 40.39 28.04), (40.39 28.04, 40.77 28.15), (40.77 28.15, 41.11 28.34), (41.11 28.34, 41.41 28.59), (41.41 28.59, 41.66 28.89), (41.66 28.89, 41.85 29.23), (41.85 29.23, 41.96 29.61), (41.96 29.61, 42 30), (41.96 30.39, 42 30), (41.85 30.77, 41.96 30.39), (41.66 31.11, 41.96 30.39), (41.41 31.41, 41.96 30.39), (41.41 28.59, 41.96 30.39), (41.41 28.59, 41.41 31.41), (38.59 28.59, 41.41 28.59), (38.59 28.59, 41.41 31.41), (38.59 28.59, 38.59 31.41), (38.59 31.41, 41.41 31.41), (38.59 31.41, 39.61 31.96), (39.61 31.96, 41.41 31.41), (39.61 31.96, 40.39 31.96), (40.39 31.96, 41.41 31.41), (40.39 31.96, 41.11 31.66), (38.04 30.39, 38.59 28.59), (38.04 30.39, 38.59 31.41), (38.04 30.39, 38.34 31.11), (38.04 29.61, 38.59 28.59), (38.04 29.61, 38.04 30.39), (39.61 28.04, 41.41 28.59), (38.59 28.59, 39.61 28.04), (38.89 28.34, 39.61 28.04), (40.39 28.04, 41.41 28.59), (39.61 28.04, 40.39 28.04), (41.96 29.61, 41.96 30.39), (41.41 28.59, 41.96 29.61), (41.66 28.89, 41.96 29.61), (40.39 28.04, 41.11 28.34), (38.04 29.61, 38.34 28.89), (38.89 31.66, 39.61 31.96))"; - runDelaunayEdges(wkt, expected); + checkDelaunayEdges(wkt, expected); } public void testPolygonWithChevronHoles() @@ -77,26 +79,88 @@ public void testPolygonWithChevronHoles() { String wkt = "POLYGON ((0 0, 0 200, 180 200, 180 0, 0 0), (20 180, 160 180, 160 20, 152.625 146.75, 20 180), (30 160, 150 30, 70 90, 30 160))"; String expected = "MULTILINESTRING ((0 200, 180 200), (0 0, 0 200), (0 0, 180 0), (180 200, 180 0), (152.625 146.75, 180 0), (152.625 146.75, 180 200), (152.625 146.75, 160 180), (160 180, 180 200), (0 200, 160 180), (20 180, 160 180), (0 200, 20 180), (20 180, 30 160), (30 160, 0 200), (0 0, 30 160), (30 160, 70 90), (0 0, 70 90), (70 90, 150 30), (150 30, 0 0), (150 30, 160 20), (0 0, 160 20), (160 20, 180 0), (152.625 146.75, 160 20), (150 30, 152.625 146.75), (70 90, 152.625 146.75), (30 160, 152.625 146.75), (30 160, 160 180))"; - runDelaunayEdges(wkt, expected); + checkDelaunayEdges(wkt, expected); } + // see https://github.com/libgeos/geos/issues/719 public void testFrameTooSmallBug() throws ParseException { String wkt = "MULTIPOINT ((0 194), (66 151), (203 80), (273 43), (340 0))"; String expected = "GEOMETRYCOLLECTION (POLYGON ((0 194, 66 151, 203 80, 0 194)), POLYGON ((0 194, 203 80, 273 43, 0 194)), POLYGON ((273 43, 203 80, 340 0, 273 43)), POLYGON ((340 0, 203 80, 66 151, 340 0)))"; - runDelaunay(wkt, true, expected); + checkDelaunay(wkt, true, expected); + } + + // see https://github.com/libgeos/geos/issues/719 + public void testNarrow_GEOS_719() + throws ParseException + { + String wkt = "MULTIPOINT ((1139294.6389832513 8201313.534695469), (1139360.8549531854 8201271.189805277), (1139497.5995843115 8201199.995542546), (1139567.7837303514 8201163.348533507), (1139635.3942210067 8201119.902527407))"; + String expected = "GEOMETRYCOLLECTION (POLYGON ((1139294.6389832513 8201313.534695469, 1139360.8549531854 8201271.189805277, 1139497.5995843115 8201199.995542546, 1139294.6389832513 8201313.534695469)), POLYGON ((1139294.6389832513 8201313.534695469, 1139497.5995843115 8201199.995542546, 1139567.7837303514 8201163.348533507, 1139294.6389832513 8201313.534695469)), POLYGON ((1139567.7837303514 8201163.348533507, 1139497.5995843115 8201199.995542546, 1139635.3942210067 8201119.902527407, 1139567.7837303514 8201163.348533507)), POLYGON ((1139635.3942210067 8201119.902527407, 1139497.5995843115 8201199.995542546, 1139360.8549531854 8201271.189805277, 1139635.3942210067 8201119.902527407)))"; + checkDelaunay(wkt, true, expected); + } + + + public void testNarrowTriangle() + throws ParseException + { + String wkt = "MULTIPOINT ((100 200), (200 190), (300 200))"; + String expected = "GEOMETRYCOLLECTION (POLYGON ((100 200, 300 200, 200 190, 100 200)))"; + checkDelaunay(wkt, true, expected); + } + + // seee https://github.com/locationtech/jts/issues/477 + public void testNarrow_GH477_1() + throws ParseException + { + String wkt = "MULTIPOINT ((0 0), (1 0), (-1 0.05), (0 0))"; + String expected = "GEOMETRYCOLLECTION (POLYGON ((-1 0.05, 1 0, 0 0, -1 0.05)))"; + checkDelaunay(wkt, true, expected); + } + + // see https://github.com/locationtech/jts/issues/477 + public void testNarrow_GH477_2() + throws ParseException + { + String wkt = "MULTIPOINT ((0 0), (0 486), (1 486), (1 22), (2 22), (2 0))"; + String expected = "GEOMETRYCOLLECTION (POLYGON ((0 0, 0 486, 1 22, 0 0)), POLYGON ((0 0, 1 22, 2 0, 0 0)), POLYGON ((0 486, 1 486, 1 22, 0 486)), POLYGON ((1 22, 1 486, 2 22, 1 22)), POLYGON ((1 22, 2 22, 2 0, 1 22)))"; + checkDelaunay(wkt, true, expected); + } + + // see https://github.com/libgeos/geos/issues/946 + public void testNarrow_GEOS_946() + throws ParseException + { + String wkt = "MULTIPOINT ((113.56577197798602 22.80081530883069),(113.565723279387 22.800815316487014),(113.56571548761124 22.80081531771092),(113.56571548780202 22.800815317674463),(113.56577197817877 22.8008153088047),(113.56577197798602 22.80081530883069))"; + String expected = "GEOMETRYCOLLECTION (POLYGON ((113.56571548761124 22.80081531771092, 113.565723279387 22.800815316487014, 113.56571548780202 22.800815317674463, 113.56571548761124 22.80081531771092)), POLYGON ((113.56571548780202 22.800815317674463, 113.565723279387 22.800815316487014, 113.56577197817877 22.8008153088047, 113.56571548780202 22.800815317674463)), POLYGON ((113.565723279387 22.800815316487014, 113.56577197798602 22.80081530883069, 113.56577197817877 22.8008153088047, 113.565723279387 22.800815316487014)))"; + checkDelaunay(wkt, true, expected); + } + + // see https://github.com/shapely/shapely/issues/1873 + public void testNarrow_Shapely_1873() + throws ParseException + { + String wkt = "MULTIPOINT ((584245.72096874 7549593.72686167), (584251.71398371 7549594.01629478), (584242.72446125 7549593.58214511), (584230.73978847 7549592.9760418), (584233.73581213 7549593.13045099), (584236.7318358 7549593.28486019), (584239.72795377 7549593.43742855), (584227.74314188 7549592.83423486))"; + String expected = "GEOMETRYCOLLECTION (POLYGON ((584227.74314188 7549592.83423486, 584233.73581213 7549593.13045099, 584230.73978847 7549592.9760418, 584227.74314188 7549592.83423486)), POLYGON ((584227.74314188 7549592.83423486, 584236.7318358 7549593.28486019, 584233.73581213 7549593.13045099, 584227.74314188 7549592.83423486)), POLYGON ((584227.74314188 7549592.83423486, 584239.72795377 7549593.43742855, 584236.7318358 7549593.28486019, 584227.74314188 7549592.83423486)), POLYGON ((584230.73978847 7549592.9760418, 584233.73581213 7549593.13045099, 584245.72096874 7549593.72686167, 584230.73978847 7549592.9760418)), POLYGON ((584230.73978847 7549592.9760418, 584245.72096874 7549593.72686167, 584251.71398371 7549594.01629478, 584230.73978847 7549592.9760418)), POLYGON ((584233.73581213 7549593.13045099, 584236.7318358 7549593.28486019, 584242.72446125 7549593.58214511, 584233.73581213 7549593.13045099)), POLYGON ((584233.73581213 7549593.13045099, 584242.72446125 7549593.58214511, 584245.72096874 7549593.72686167, 584233.73581213 7549593.13045099)), POLYGON ((584236.7318358 7549593.28486019, 584239.72795377 7549593.43742855, 584242.72446125 7549593.58214511, 584236.7318358 7549593.28486019)))"; + checkDelaunay(wkt, true, expected); + } + + public void testNarrowPoints() + throws ParseException + { + String wkt = "MULTIPOINT ((2 204), (3 66), (1 96), (0 236), (3 173), (2 114), (3 201), (0 46), (1 181))"; + checkDelaunayHull(wkt); } static final double COMPARISON_TOLERANCE = 1.0e-7; - void runDelaunayEdges(String sitesWKT, String expectedWKT) + void checkDelaunayEdges(String sitesWKT, String expectedWKT) throws ParseException { - runDelaunay(sitesWKT, false, expectedWKT); + checkDelaunay(sitesWKT, false, expectedWKT); } - void runDelaunay(String sitesWKT, boolean computeTriangles, String expectedWKT) + void checkDelaunay(String sitesWKT, boolean computeTriangles, String expectedWKT) throws ParseException { Geometry sites = reader.read(sitesWKT); @@ -115,6 +179,26 @@ void runDelaunay(String sitesWKT, boolean computeTriangles, String expectedWKT) Geometry expected = reader.read(expectedWKT); result.normalize(); expected.normalize(); - assertTrue(expected.equalsExact(result, COMPARISON_TOLERANCE)); + checkEqual(expected, result, COMPARISON_TOLERANCE); + } + + void checkDelaunayHull(String sitesWKT) + { + Geometry sites = read(sitesWKT); + DelaunayTriangulationBuilder builder = new DelaunayTriangulationBuilder(); + builder.setSites(sites); + + Geometry result = builder.getTriangles(geomFact); + + //System.out.println(result); + + Geometry union = CoverageUnion.union(result); + ConvexHull ch = new ConvexHull(result); + Geometry convexHull = ch.getConvexHull(); + + //boolean isEqual = union.norm().equalsExact(convexHull.norm()); + boolean isEqual = union.equalsTopo(convexHull); + + assertTrue("hulls do not match", isEqual); } } diff --git a/modules/core/src/test/java/org/locationtech/jts/triangulate/VoronoiTest.java b/modules/core/src/test/java/org/locationtech/jts/triangulate/VoronoiTest.java index 6b978030f1..4a2d149e26 100644 --- a/modules/core/src/test/java/org/locationtech/jts/triangulate/VoronoiTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/triangulate/VoronoiTest.java @@ -74,6 +74,6 @@ void runVoronoi(String sitesWKT, boolean computePolys, String expectedWKT) Geometry expected = read(expectedWKT); result.normalize(); expected.normalize(); - assertTrue(expected.equalsExact(result, COMPARISON_TOLERANCE)); + checkEqual(expected, result, COMPARISON_TOLERANCE); } } diff --git a/modules/core/src/test/java/test/jts/perf/triangulate/DelaunayStressTest.java b/modules/core/src/test/java/test/jts/perf/triangulate/DelaunayStressTest.java new file mode 100644 index 0000000000..5ebed18079 --- /dev/null +++ b/modules/core/src/test/java/test/jts/perf/triangulate/DelaunayStressTest.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package test.jts.perf.triangulate; + +import java.util.ArrayList; +import java.util.List; + +import org.locationtech.jts.algorithm.ConvexHull; +import org.locationtech.jts.algorithm.Orientation; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.geom.PrecisionModel; +import org.locationtech.jts.operation.overlayng.CoverageUnion; +import org.locationtech.jts.triangulate.DelaunayTriangulationBuilder; +import org.locationtech.jts.triangulate.VoronoiDiagramBuilder; +import org.locationtech.jts.util.Memory; +import org.locationtech.jts.util.Stopwatch; + +/** + * Test correctness of Delaunay computation with + * synthetic random datasets. + * + * @author Martin Davis + * + */ +public class DelaunayStressTest +{ + private static final int N_PTS = 50; + private static final int RUN_COUNT = 10000; + final static double SIDE_LEN = 1000.0; + final static double BASE_OFFSET = 0; + + public static void main(String args[]) { + DelaunayStressTest test = new DelaunayStressTest(); + test.run(); + } + + final static GeometryFactory geomFact = new GeometryFactory(); + private static final double WIDTH = 100; + private static final double HEIGHT = 100; + + + public void run() + { + for (int i = 0; i < RUN_COUNT; i++) { + System.out.println("Run # " + i); + run(N_PTS); + } + } + + public void run(int nPts) + { + List pts = randomPointsInGrid(nPts, BASE_OFFSET, BASE_OFFSET, WIDTH, HEIGHT, 1); + run(pts); + } + + public void run(List pts) + { + System.out.println("Base offset: " + BASE_OFFSET); + System.out.println("# pts: " + pts.size()); + Stopwatch sw = new Stopwatch(); + DelaunayTriangulationBuilder builder = new DelaunayTriangulationBuilder(); + builder.setSites(pts); + + Geometry tris = builder.getTriangles(geomFact); + checkDelaunay(tris); + + checkVoronoi(pts); + + System.out.println(" -- Time: " + sw.getTimeString() + + " Mem: " + Memory.usedTotalString()); +// System.out.println(g); + } + + private void checkVoronoi(List pts) { + VoronoiDiagramBuilder vdb = new VoronoiDiagramBuilder(); + vdb.setSites(pts); + vdb.getDiagram(geomFact); + + //-- for now simply confirm the Voronoi is computed with no failure + } + + private void checkDelaunay(Geometry tris) { + //TODO: check all elements are triangles + + //-- check triangulation is a coverage + //-- this will error if triangulation is not a valid coverage + Geometry union = CoverageUnion.union(tris); + + checkConvex(tris, union); + } + + private void checkConvex(Geometry tris, Geometry triHull) { + Geometry convexHull = convexHull(tris); + boolean isEqual = triHull.equalsTopo(convexHull); + + boolean isConvex = isConvex((Polygon) triHull); + + if (! isConvex) { + System.out.println("Tris:"); + System.out.println(tris); + System.out.println("Convex Hull:"); + System.out.println(convexHull); + throw new IllegalStateException("Delaunay triangulation is not convex"); + } + } + + private Geometry convexHull(Geometry tris) { + ConvexHull hull = new ConvexHull(tris); + return hull.getConvexHull(); + } + + private boolean isConvex(Polygon poly) { + Coordinate[] pts = poly.getCoordinates(); + for (int i = 0; i < pts.length - 1; i++) { + int iprev = i - 1; + if (iprev < 0) iprev = pts.length - 2; + int inext = i + 1; + //-- orientation must be CLOCKWISE or COLLINEAR + boolean isConvex = Orientation.COUNTERCLOCKWISE != Orientation.index(pts[iprev], pts[i], pts[inext]); + if (! isConvex) + return false; + } + return true; + } + static List randomPointsInGrid(int nPts, double basex, double basey, double width, double height, double scale) + { + PrecisionModel pm = null; + if (scale > 0) { + pm = new PrecisionModel(scale); + } + List pts = new ArrayList(); + + int nSide = (int) Math.sqrt(nPts) + 1; + + for (int i = 0; i < nSide; i++) { + for (int j = 0; j < nSide; j++) { + double x = basex + i * width + width * Math.random(); + double y = basey + j * height + height * Math.random(); + Coordinate p = new Coordinate(x, y); + round(p, pm); + pts.add(p); + } + } + return pts; + } + + private static void round(Coordinate p, PrecisionModel pm) { + if (pm == null) + return; + pm.makePrecise(p); + } + + static List randomPoints(int nPts, double sideLen) + { + List pts = new ArrayList(); + + for (int i = 0; i < nPts; i++) { + double x = sideLen * Math.random(); + double y = sideLen * Math.random(); + pts.add(new Coordinate(x, y)); + } + return pts; + } +} From 53d1cd2214931ef146b96234bddeb223fa83080c Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 29 Aug 2023 16:33:13 -0700 Subject: [PATCH 006/123] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 83b5e2c07e..a3b6954669 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -58,6 +58,7 @@ Distributions for older JTS versions can be obtained at the * Fix `LargestEmptyCircle` to handle polygonal obstacles (#988) * Make intersection computation more robust (#989) * Fix `VariableBuffer` to handle zero vertex buffer distances correctly (#997) +* Fix IncrementalDelaunayTriangulator to ensure triangulation boundary is convex (#1004) ### Performance Improvements From 159b39bd9f830d837febfe1295ddbead86446e28 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 30 Aug 2023 15:54:30 -0700 Subject: [PATCH 007/123] Add Voronoi test for disabling frame convexity --- .../org/locationtech/jts/triangulate/VoronoiTest.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/modules/core/src/test/java/org/locationtech/jts/triangulate/VoronoiTest.java b/modules/core/src/test/java/org/locationtech/jts/triangulate/VoronoiTest.java index 4a2d149e26..08bd2f0da7 100644 --- a/modules/core/src/test/java/org/locationtech/jts/triangulate/VoronoiTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/triangulate/VoronoiTest.java @@ -52,6 +52,16 @@ public void testSitesWithPointsOnSquareGrid() { runVoronoi(wkt); } + /** + * This test fails if the frame is forced to be convex via {@link IncrementalDelaunayTriangulator#forceConvex(boolean)}. + * It is also dependent on the frame size factor - a value of 10 causes failure, + * but larger values may not. + */ + public void testFrameDisableForceConvex() { + String wkt = "MULTIPOINT ((259 289), (46 194), (396 359), (243 349), (206 99), (470 40), (429 185), (54 9), (78 208), (457 406), (355 191), (346 497), (144 79), (35 459), (322 37), (181 371), (359 257), (57 331), (225 139), (475 245), (416 364), (155 477), (123 232), (102 141), (251 434))"; + runVoronoi(wkt); + } + static final double COMPARISON_TOLERANCE = 1.0e-7; private void runVoronoi(String sitesWKT) { From 895dac87d855c0385c49ab45d15027402d05ec0e Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 31 Aug 2023 12:53:59 -0700 Subject: [PATCH 008/123] Add ConcaveHull robustness tests --- .../jts/algorithm/hull/ConcaveHullTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java index dcece33f37..b5bcaed16a 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java @@ -128,6 +128,21 @@ public void testAlphaWithHolesCircle() { "POLYGON ((20 90, 40 96, 56 95, 80 90, 90 70, 95 45, 90 20, 80 10, 60 15, 45 5, 20 10, 10 20, 5 40, 11 60, 20 70, 20 90), (40 80, 15 45, 21 30, 40 20, 70 20, 80 40, 80 60, 70 80, 40 80))" ); } + //------------------------------------------------ + + // These tests test that the computed Delaunay triangulation is correct + // See https://github.com/locationtech/jts/pull/1004 + + public void testRobust_GEOS946() { + checkHullByLengthRatio("MULTIPOINT ((113.56577197798602 22.80081530883069),(113.565723279387 22.800815316487014),(113.56571548761124 22.80081531771092),(113.56571548780202 22.800815317674463),(113.56577197817877 22.8008153088047),(113.56577197798602 22.80081530883069))", + 0.75, "POLYGON ((113.56571548761124 22.80081531771092, 113.565723279387 22.800815316487014, 113.56577197798602 22.80081530883069, 113.56577197817877 22.8008153088047, 113.56571548780202 22.800815317674463, 113.56571548761124 22.80081531771092))" ); + } + + public void testRobust_GEOS946_2() { + checkHullByLengthRatio("MULTIPOINT ((584245.72096874 7549593.72686167), (584251.71398371 7549594.01629478), (584242.72446125 7549593.58214511), (584230.73978847 7549592.9760418), (584233.73581213 7549593.13045099), (584236.7318358 7549593.28486019), (584239.72795377 7549593.43742855), (584227.74314188 7549592.83423486))", + 0.75, "POLYGON ((584227.74314188 7549592.83423486, 584239.72795377 7549593.43742855, 584242.72446125 7549593.58214511, 584245.72096874 7549593.72686167, 584251.71398371 7549594.01629478, 584230.73978847 7549592.9760418, 584227.74314188 7549592.83423486))" ); + } + //========================================================================== private void checkHullByLengthRatio(String wkt, double threshold, String wktExpected) { From b5e074cefb89a1bb72f74430dc5a6574138be59c Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 31 Aug 2023 13:13:03 -0700 Subject: [PATCH 009/123] Simplify IncrementalDelaunayTriangulator code --- .../jts/triangulate/IncrementalDelaunayTriangulator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/triangulate/IncrementalDelaunayTriangulator.java b/modules/core/src/main/java/org/locationtech/jts/triangulate/IncrementalDelaunayTriangulator.java index 17f794944b..8bb4de1af9 100644 --- a/modules/core/src/main/java/org/locationtech/jts/triangulate/IncrementalDelaunayTriangulator.java +++ b/modules/core/src/main/java/org/locationtech/jts/triangulate/IncrementalDelaunayTriangulator.java @@ -158,9 +158,9 @@ else if (isBetweenFrameAndInserted(e, v)) { if (e.oNext() == startEdge) { return base; // no more suspect edges. - } else { - e = e.oNext().lPrev(); } + //-- check next edge + e = e.oNext().lPrev(); } while (true); } From ee59b591f15b5150516393d3ba0b49e46a113fc9 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 31 Aug 2023 17:53:00 -0700 Subject: [PATCH 010/123] Clean up DelaunayTest code --- .../jts/triangulate/DelaunayTest.java | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/modules/core/src/test/java/org/locationtech/jts/triangulate/DelaunayTest.java b/modules/core/src/test/java/org/locationtech/jts/triangulate/DelaunayTest.java index 7443ef5061..be0b90ead6 100644 --- a/modules/core/src/test/java/org/locationtech/jts/triangulate/DelaunayTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/triangulate/DelaunayTest.java @@ -14,8 +14,6 @@ import org.locationtech.jts.algorithm.ConvexHull; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; -import org.locationtech.jts.io.ParseException; -import org.locationtech.jts.io.WKTReader; import org.locationtech.jts.operation.overlayng.CoverageUnion; import junit.textui.TestRunner; @@ -32,12 +30,10 @@ public static void main(String args[]) { } private GeometryFactory geomFact = new GeometryFactory(); - private WKTReader reader = new WKTReader(); public DelaunayTest(String name) { super(name); } public void testTriangle() - throws ParseException { String wkt = "MULTIPOINT ((10 10 1), (10 20 2), (20 20 3))"; String expected = "MULTILINESTRING ((10 20, 20 20), (10 10, 10 20), (10 10, 20 20))"; @@ -47,7 +43,6 @@ public void testTriangle() } public void testRandom() - throws ParseException { String wkt = "MULTIPOINT ((50 40), (140 70), (80 100), (130 140), (30 150), (70 180), (190 110), (120 20))"; String expected = "MULTILINESTRING ((70 180, 190 110), (30 150, 70 180), (30 150, 50 40), (50 40, 120 20), (190 110, 120 20), (120 20, 140 70), (190 110, 140 70), (130 140, 140 70), (130 140, 190 110), (70 180, 130 140), (80 100, 130 140), (70 180, 80 100), (30 150, 80 100), (50 40, 80 100), (80 100, 120 20), (80 100, 140 70))"; @@ -57,7 +52,6 @@ public void testRandom() } public void testGrid() - throws ParseException { String wkt = "MULTIPOINT ((10 10), (10 20), (20 20), (20 10), (20 0), (10 0), (0 0), (0 10), (0 20))"; String expected = "MULTILINESTRING ((10 20, 20 20), (0 20, 10 20), (0 10, 0 20), (0 0, 0 10), (0 0, 10 0), (10 0, 20 0), (20 0, 20 10), (20 10, 20 20), (10 20, 20 10), (10 10, 20 10), (10 10, 10 20), (10 10, 0 20), (10 10, 0 10), (10 0, 10 10), (0 10, 10 0), (10 10, 20 0))"; @@ -67,7 +61,6 @@ public void testGrid() } public void testCircle() - throws ParseException { String wkt = "POLYGON ((42 30, 41.96 29.61, 41.85 29.23, 41.66 28.89, 41.41 28.59, 41.11 28.34, 40.77 28.15, 40.39 28.04, 40 28, 39.61 28.04, 39.23 28.15, 38.89 28.34, 38.59 28.59, 38.34 28.89, 38.15 29.23, 38.04 29.61, 38 30, 38.04 30.39, 38.15 30.77, 38.34 31.11, 38.59 31.41, 38.89 31.66, 39.23 31.85, 39.61 31.96, 40 32, 40.39 31.96, 40.77 31.85, 41.11 31.66, 41.41 31.41, 41.66 31.11, 41.85 30.77, 41.96 30.39, 42 30))"; String expected = "MULTILINESTRING ((41.66 31.11, 41.85 30.77), (41.41 31.41, 41.66 31.11), (41.11 31.66, 41.41 31.41), (40.77 31.85, 41.11 31.66), (40.39 31.96, 40.77 31.85), (40 32, 40.39 31.96), (39.61 31.96, 40 32), (39.23 31.85, 39.61 31.96), (38.89 31.66, 39.23 31.85), (38.59 31.41, 38.89 31.66), (38.34 31.11, 38.59 31.41), (38.15 30.77, 38.34 31.11), (38.04 30.39, 38.15 30.77), (38 30, 38.04 30.39), (38 30, 38.04 29.61), (38.04 29.61, 38.15 29.23), (38.15 29.23, 38.34 28.89), (38.34 28.89, 38.59 28.59), (38.59 28.59, 38.89 28.34), (38.89 28.34, 39.23 28.15), (39.23 28.15, 39.61 28.04), (39.61 28.04, 40 28), (40 28, 40.39 28.04), (40.39 28.04, 40.77 28.15), (40.77 28.15, 41.11 28.34), (41.11 28.34, 41.41 28.59), (41.41 28.59, 41.66 28.89), (41.66 28.89, 41.85 29.23), (41.85 29.23, 41.96 29.61), (41.96 29.61, 42 30), (41.96 30.39, 42 30), (41.85 30.77, 41.96 30.39), (41.66 31.11, 41.96 30.39), (41.41 31.41, 41.96 30.39), (41.41 28.59, 41.96 30.39), (41.41 28.59, 41.41 31.41), (38.59 28.59, 41.41 28.59), (38.59 28.59, 41.41 31.41), (38.59 28.59, 38.59 31.41), (38.59 31.41, 41.41 31.41), (38.59 31.41, 39.61 31.96), (39.61 31.96, 41.41 31.41), (39.61 31.96, 40.39 31.96), (40.39 31.96, 41.41 31.41), (40.39 31.96, 41.11 31.66), (38.04 30.39, 38.59 28.59), (38.04 30.39, 38.59 31.41), (38.04 30.39, 38.34 31.11), (38.04 29.61, 38.59 28.59), (38.04 29.61, 38.04 30.39), (39.61 28.04, 41.41 28.59), (38.59 28.59, 39.61 28.04), (38.89 28.34, 39.61 28.04), (40.39 28.04, 41.41 28.59), (39.61 28.04, 40.39 28.04), (41.96 29.61, 41.96 30.39), (41.41 28.59, 41.96 29.61), (41.66 28.89, 41.96 29.61), (40.39 28.04, 41.11 28.34), (38.04 29.61, 38.34 28.89), (38.89 31.66, 39.61 31.96))"; @@ -75,7 +68,6 @@ public void testCircle() } public void testPolygonWithChevronHoles() - throws ParseException { String wkt = "POLYGON ((0 0, 0 200, 180 200, 180 0, 0 0), (20 180, 160 180, 160 20, 152.625 146.75, 20 180), (30 160, 150 30, 70 90, 30 160))"; String expected = "MULTILINESTRING ((0 200, 180 200), (0 0, 0 200), (0 0, 180 0), (180 200, 180 0), (152.625 146.75, 180 0), (152.625 146.75, 180 200), (152.625 146.75, 160 180), (160 180, 180 200), (0 200, 160 180), (20 180, 160 180), (0 200, 20 180), (20 180, 30 160), (30 160, 0 200), (0 0, 30 160), (30 160, 70 90), (0 0, 70 90), (70 90, 150 30), (150 30, 0 0), (150 30, 160 20), (0 0, 160 20), (160 20, 180 0), (152.625 146.75, 160 20), (150 30, 152.625 146.75), (70 90, 152.625 146.75), (30 160, 152.625 146.75), (30 160, 160 180))"; @@ -84,7 +76,6 @@ public void testPolygonWithChevronHoles() // see https://github.com/libgeos/geos/issues/719 public void testFrameTooSmallBug() - throws ParseException { String wkt = "MULTIPOINT ((0 194), (66 151), (203 80), (273 43), (340 0))"; String expected = "GEOMETRYCOLLECTION (POLYGON ((0 194, 66 151, 203 80, 0 194)), POLYGON ((0 194, 203 80, 273 43, 0 194)), POLYGON ((273 43, 203 80, 340 0, 273 43)), POLYGON ((340 0, 203 80, 66 151, 340 0)))"; @@ -93,7 +84,6 @@ public void testFrameTooSmallBug() // see https://github.com/libgeos/geos/issues/719 public void testNarrow_GEOS_719() - throws ParseException { String wkt = "MULTIPOINT ((1139294.6389832513 8201313.534695469), (1139360.8549531854 8201271.189805277), (1139497.5995843115 8201199.995542546), (1139567.7837303514 8201163.348533507), (1139635.3942210067 8201119.902527407))"; String expected = "GEOMETRYCOLLECTION (POLYGON ((1139294.6389832513 8201313.534695469, 1139360.8549531854 8201271.189805277, 1139497.5995843115 8201199.995542546, 1139294.6389832513 8201313.534695469)), POLYGON ((1139294.6389832513 8201313.534695469, 1139497.5995843115 8201199.995542546, 1139567.7837303514 8201163.348533507, 1139294.6389832513 8201313.534695469)), POLYGON ((1139567.7837303514 8201163.348533507, 1139497.5995843115 8201199.995542546, 1139635.3942210067 8201119.902527407, 1139567.7837303514 8201163.348533507)), POLYGON ((1139635.3942210067 8201119.902527407, 1139497.5995843115 8201199.995542546, 1139360.8549531854 8201271.189805277, 1139635.3942210067 8201119.902527407)))"; @@ -102,7 +92,6 @@ public void testNarrow_GEOS_719() public void testNarrowTriangle() - throws ParseException { String wkt = "MULTIPOINT ((100 200), (200 190), (300 200))"; String expected = "GEOMETRYCOLLECTION (POLYGON ((100 200, 300 200, 200 190, 100 200)))"; @@ -111,7 +100,6 @@ public void testNarrowTriangle() // seee https://github.com/locationtech/jts/issues/477 public void testNarrow_GH477_1() - throws ParseException { String wkt = "MULTIPOINT ((0 0), (1 0), (-1 0.05), (0 0))"; String expected = "GEOMETRYCOLLECTION (POLYGON ((-1 0.05, 1 0, 0 0, -1 0.05)))"; @@ -120,7 +108,6 @@ public void testNarrow_GH477_1() // see https://github.com/locationtech/jts/issues/477 public void testNarrow_GH477_2() - throws ParseException { String wkt = "MULTIPOINT ((0 0), (0 486), (1 486), (1 22), (2 22), (2 0))"; String expected = "GEOMETRYCOLLECTION (POLYGON ((0 0, 0 486, 1 22, 0 0)), POLYGON ((0 0, 1 22, 2 0, 0 0)), POLYGON ((0 486, 1 486, 1 22, 0 486)), POLYGON ((1 22, 1 486, 2 22, 1 22)), POLYGON ((1 22, 2 22, 2 0, 1 22)))"; @@ -129,7 +116,6 @@ public void testNarrow_GH477_2() // see https://github.com/libgeos/geos/issues/946 public void testNarrow_GEOS_946() - throws ParseException { String wkt = "MULTIPOINT ((113.56577197798602 22.80081530883069),(113.565723279387 22.800815316487014),(113.56571548761124 22.80081531771092),(113.56571548780202 22.800815317674463),(113.56577197817877 22.8008153088047),(113.56577197798602 22.80081530883069))"; String expected = "GEOMETRYCOLLECTION (POLYGON ((113.56571548761124 22.80081531771092, 113.565723279387 22.800815316487014, 113.56571548780202 22.800815317674463, 113.56571548761124 22.80081531771092)), POLYGON ((113.56571548780202 22.800815317674463, 113.565723279387 22.800815316487014, 113.56577197817877 22.8008153088047, 113.56571548780202 22.800815317674463)), POLYGON ((113.565723279387 22.800815316487014, 113.56577197798602 22.80081530883069, 113.56577197817877 22.8008153088047, 113.565723279387 22.800815316487014)))"; @@ -138,7 +124,6 @@ public void testNarrow_GEOS_946() // see https://github.com/shapely/shapely/issues/1873 public void testNarrow_Shapely_1873() - throws ParseException { String wkt = "MULTIPOINT ((584245.72096874 7549593.72686167), (584251.71398371 7549594.01629478), (584242.72446125 7549593.58214511), (584230.73978847 7549592.9760418), (584233.73581213 7549593.13045099), (584236.7318358 7549593.28486019), (584239.72795377 7549593.43742855), (584227.74314188 7549592.83423486))"; String expected = "GEOMETRYCOLLECTION (POLYGON ((584227.74314188 7549592.83423486, 584233.73581213 7549593.13045099, 584230.73978847 7549592.9760418, 584227.74314188 7549592.83423486)), POLYGON ((584227.74314188 7549592.83423486, 584236.7318358 7549593.28486019, 584233.73581213 7549593.13045099, 584227.74314188 7549592.83423486)), POLYGON ((584227.74314188 7549592.83423486, 584239.72795377 7549593.43742855, 584236.7318358 7549593.28486019, 584227.74314188 7549592.83423486)), POLYGON ((584230.73978847 7549592.9760418, 584233.73581213 7549593.13045099, 584245.72096874 7549593.72686167, 584230.73978847 7549592.9760418)), POLYGON ((584230.73978847 7549592.9760418, 584245.72096874 7549593.72686167, 584251.71398371 7549594.01629478, 584230.73978847 7549592.9760418)), POLYGON ((584233.73581213 7549593.13045099, 584236.7318358 7549593.28486019, 584242.72446125 7549593.58214511, 584233.73581213 7549593.13045099)), POLYGON ((584233.73581213 7549593.13045099, 584242.72446125 7549593.58214511, 584245.72096874 7549593.72686167, 584233.73581213 7549593.13045099)), POLYGON ((584236.7318358 7549593.28486019, 584239.72795377 7549593.43742855, 584242.72446125 7549593.58214511, 584236.7318358 7549593.28486019)))"; @@ -146,7 +131,6 @@ public void testNarrow_Shapely_1873() } public void testNarrowPoints() - throws ParseException { String wkt = "MULTIPOINT ((2 204), (3 66), (1 96), (0 236), (3 173), (2 114), (3 201), (0 46), (1 181))"; checkDelaunayHull(wkt); @@ -155,15 +139,13 @@ public void testNarrowPoints() static final double COMPARISON_TOLERANCE = 1.0e-7; void checkDelaunayEdges(String sitesWKT, String expectedWKT) - throws ParseException { checkDelaunay(sitesWKT, false, expectedWKT); } void checkDelaunay(String sitesWKT, boolean computeTriangles, String expectedWKT) - throws ParseException { - Geometry sites = reader.read(sitesWKT); + Geometry sites = read(sitesWKT); DelaunayTriangulationBuilder builder = new DelaunayTriangulationBuilder(); builder.setSites(sites); @@ -176,7 +158,7 @@ void checkDelaunay(String sitesWKT, boolean computeTriangles, String expectedWKT } //System.out.println(result); - Geometry expected = reader.read(expectedWKT); + Geometry expected = read(expectedWKT); result.normalize(); expected.normalize(); checkEqual(expected, result, COMPARISON_TOLERANCE); From 52e19880a045c90991f17b2cc655abd94117622b Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Sat, 16 Sep 2023 20:06:37 -0700 Subject: [PATCH 011/123] Add TestBuilder function isWithinDistanceIndexed --- .../org/locationtech/jtstest/function/DistanceFunctions.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/DistanceFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/DistanceFunctions.java index 367bf706a5..e211bbeff3 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/DistanceFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/DistanceFunctions.java @@ -82,6 +82,10 @@ public static double distanceIndexed(Geometry a, Geometry b) { return IndexedFacetDistance.distance(a, b); } + public static boolean isWithinDistanceIndexed(Geometry a, Geometry b, double distance) { + return IndexedFacetDistance.isWithinDistance(a, b, distance); + } + public static Geometry nearestPointsIndexed(Geometry a, Geometry b) { Coordinate[] pts = IndexedFacetDistance.nearestPoints(a, b); return a.getFactory().createLineString(pts); From ee68200490d79d84ce843847d8a3a232eeab272e Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 17 Oct 2023 12:55:20 -0700 Subject: [PATCH 012/123] Improve OverlayNG difference check heuristic (#1005) --- .../jts/operation/overlayng/OverlayUtil.java | 22 +++++++++++++++++-- .../overlayng/OverlayNGSnappingNoderTest.java | 17 ++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/OverlayUtil.java b/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/OverlayUtil.java index 6265bd0e38..dd82a8de2d 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/OverlayUtil.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/OverlayUtil.java @@ -392,6 +392,8 @@ public static boolean isResultAreaConsistent(Geometry geom0, Geometry geom1, int if (geom0 == null || geom1 == null) return true; + if (result.getDimension() < 2) return true; + double areaResult = result.getArea(); double areaA = geom0.getArea(); double areaB = geom1.getArea(); @@ -403,8 +405,7 @@ public static boolean isResultAreaConsistent(Geometry geom0, Geometry geom1, int && isLess(areaResult, areaB, AREA_HEURISTIC_TOLERANCE); break; case OverlayNG.DIFFERENCE: - isConsistent = isLess(areaResult, areaA, AREA_HEURISTIC_TOLERANCE) - && isGreater(areaResult, areaA - areaB, AREA_HEURISTIC_TOLERANCE); + isConsistent = isDifferenceAreaConsistent(areaA, areaB, areaResult, AREA_HEURISTIC_TOLERANCE); break; case OverlayNG.SYMDIFFERENCE: isConsistent = isLess(areaResult, areaA + areaB, AREA_HEURISTIC_TOLERANCE); @@ -417,6 +418,23 @@ && isLess(areaB, areaResult, AREA_HEURISTIC_TOLERANCE) } return isConsistent; } + + /** + * Tests if the area of a difference is greater than the minimum possible difference area. + * This is a heuristic which will only detect gross overlay errors. + * @param areaA the area of A + * @param areaB the area of B + * @param areaResult the result area + * @param tolFrac the area tolerance fraction + * + * @return true if the difference area is consistent. + */ + private static boolean isDifferenceAreaConsistent(double areaA, double areaB, double areaResult, double tolFrac) { + if (! isLess(areaResult, areaA, tolFrac)) + return false; + double areaDiffMin = areaA - areaB - tolFrac * areaA; + return areaResult > areaDiffMin; + } private static boolean isLess(double v1, double v2, double tol) { return v1 <= v2 * (1 + tol); diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGSnappingNoderTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGSnappingNoderTest.java index f8e1e2ccf9..06de1b7b14 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGSnappingNoderTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGSnappingNoderTest.java @@ -11,6 +11,7 @@ */ package org.locationtech.jts.operation.overlayng; +import static org.locationtech.jts.operation.overlayng.OverlayNG.DIFFERENCE; import static org.locationtech.jts.operation.overlayng.OverlayNG.INTERSECTION; import static org.locationtech.jts.operation.overlayng.OverlayNG.UNION; @@ -70,11 +71,27 @@ public void testTrianglesBSegmentsDisplacedUnion() { checkEqual(expected, union(a, b, 0.1)); } + /** + * Failing due to OverlayUtil#isResultAreaConsistent + * See https://github.com/locationtech/jts/issues/951 + */ + public void testRotatedVerticesDifference() { + Geometry a = read("POLYGON ((0.37676311 2.57570853, 7.28652472 0.00028375, 7.60034931 0.81686059, 0.50229292 3.4551325, 0.37676311 2.57570853))"); + Geometry b = read("POLYGON ((0.50229292 3.4551325, 7.60034931 0.81686059, 7.28652472 0.00028375, 0.37676311 2.57570853, 0.50229292 3.4551325))"); + Geometry expected = read("POLYGON EMPTY"); + checkEqual(expected, difference(a, b, 0.00001)); + } + public static Geometry union(Geometry a, Geometry b, double tolerance) { Noder noder = getNoder(tolerance); return OverlayNG.overlay(a, b, UNION, null, noder ); } + public static Geometry difference(Geometry a, Geometry b, double tolerance) { + Noder noder = getNoder(tolerance); + return OverlayNG.overlay(a, b, DIFFERENCE, null, noder ); + } + private static Noder getNoder(double tolerance) { SnappingNoder snapNoder = new SnappingNoder(tolerance); return new ValidatingNoder(snapNoder); From 566a072a9deed36c9d1d8a7d88b8b5ea7e35d8a6 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 17 Oct 2023 12:57:26 -0700 Subject: [PATCH 013/123] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index a3b6954669..8003798e82 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -59,6 +59,7 @@ Distributions for older JTS versions can be obtained at the * Make intersection computation more robust (#989) * Fix `VariableBuffer` to handle zero vertex buffer distances correctly (#997) * Fix IncrementalDelaunayTriangulator to ensure triangulation boundary is convex (#1004) +* Fix OverlayNG Area Check heuristic for difference (#1005) ### Performance Improvements From ed8d462e19a67bc278a7ed00b79cff9a1adee861 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Sun, 29 Oct 2023 07:38:04 -0700 Subject: [PATCH 014/123] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6bb4bd7eb4..9690ae3a73 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ If you are interested in contributing to JTS please read the [**Contributing Gui * [**GEOS**](https://trac.osgeo.org/geos) - C++ * [**NetTopologySuite**](https://github.com/NetTopologySuite/NetTopologySuite) - .NET * [**JSTS**](https://github.com/bjornharrtell/jsts) - JavaScript -* [**dart_jts]([https://pub.dev/packages/dart_jts](https://github.com/moovida/dart_jts)) - Dart +* [**dart_jts]([https://pub.dev/packages/dart_jts](https://github.com/moovida/dart_jts) - Dart ### Via GEOS * [**Shapely**](https://github.com/Toblerity/Shapely) - Python wrapper of GEOS From 8d8d6d28fec95d687ae3df7a24410f6d56991023 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Sun, 29 Oct 2023 07:38:35 -0700 Subject: [PATCH 015/123] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9690ae3a73..4aace6ab55 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ If you are interested in contributing to JTS please read the [**Contributing Gui * [**GEOS**](https://trac.osgeo.org/geos) - C++ * [**NetTopologySuite**](https://github.com/NetTopologySuite/NetTopologySuite) - .NET * [**JSTS**](https://github.com/bjornharrtell/jsts) - JavaScript -* [**dart_jts]([https://pub.dev/packages/dart_jts](https://github.com/moovida/dart_jts) - Dart +* [**dart_jts**]([https://pub.dev/packages/dart_jts](https://github.com/moovida/dart_jts) - Dart ### Via GEOS * [**Shapely**](https://github.com/Toblerity/Shapely) - Python wrapper of GEOS From 5a0d53891b193170b3594cc70d88b4f68e3cd6b1 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Sun, 29 Oct 2023 07:39:29 -0700 Subject: [PATCH 016/123] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4aace6ab55..2e897c4bc3 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ If you are interested in contributing to JTS please read the [**Contributing Gui * [**GEOS**](https://trac.osgeo.org/geos) - C++ * [**NetTopologySuite**](https://github.com/NetTopologySuite/NetTopologySuite) - .NET * [**JSTS**](https://github.com/bjornharrtell/jsts) - JavaScript -* [**dart_jts**]([https://pub.dev/packages/dart_jts](https://github.com/moovida/dart_jts) - Dart +* [**dart_jts**](https://github.com/moovida/dart_jts) - Dart ### Via GEOS * [**Shapely**](https://github.com/Toblerity/Shapely) - Python wrapper of GEOS From 70817f5353669cebd7474be87bb2a06b3b603dfe Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Sun, 29 Oct 2023 10:19:40 -0700 Subject: [PATCH 017/123] Fix InteriorPointPoint to handle empty elements --- doc/JTS_Version_History.md | 1 + .../locationtech/jts/algorithm/InteriorPointPoint.java | 3 +++ .../test/resources/testxml/general/TestInteriorPoint.xml | 9 ++++++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 8003798e82..cff62ea8c0 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -60,6 +60,7 @@ Distributions for older JTS versions can be obtained at the * Fix `VariableBuffer` to handle zero vertex buffer distances correctly (#997) * Fix IncrementalDelaunayTriangulator to ensure triangulation boundary is convex (#1004) * Fix OverlayNG Area Check heuristic for difference (#1005) +* Fix InteriorPointPoint to handle empty elements ### Performance Improvements diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPointPoint.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPointPoint.java index 23a0243062..2c6169b3c8 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPointPoint.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPointPoint.java @@ -56,6 +56,9 @@ public InteriorPointPoint(Geometry g) */ private void add(Geometry geom) { + if (geom.isEmpty()) + return; + if (geom instanceof Point) { add(geom.getCoordinate()); } diff --git a/modules/tests/src/test/resources/testxml/general/TestInteriorPoint.xml b/modules/tests/src/test/resources/testxml/general/TestInteriorPoint.xml index ed601d481a..9c3cbdc05b 100644 --- a/modules/tests/src/test/resources/testxml/general/TestInteriorPoint.xml +++ b/modules/tests/src/test/resources/testxml/general/TestInteriorPoint.xml @@ -14,12 +14,19 @@ - P - single point + P - multipoint MULTIPOINT ((60 300), (200 200), (240 240), (200 300), (40 140), (80 240), (140 240), (100 160), (140 200), (60 200)) POINT (140 240) + + P - multipoint with EMPTY + MULTIPOINT((0 0), EMPTY) + + POINT (0 0) + + L - empty LINESTRING EMPTY From 001500addccc64ccc537cd46056e8b7b4738561f Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 31 Oct 2023 16:34:59 -0700 Subject: [PATCH 018/123] Fix DistanceOp for empty elements (#1010) --- .../jts/operation/distance/DistanceOp.java | 6 ++ .../testxml/general/TestDistance.xml | 67 ++++++++++++++++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/distance/DistanceOp.java b/modules/core/src/main/java/org/locationtech/jts/operation/distance/DistanceOp.java index 34542df972..cca020c846 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/distance/DistanceOp.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/distance/DistanceOp.java @@ -348,8 +348,12 @@ private void computeMinDistancePoints(List points0, List points1, GeometryLocati { for (int i = 0; i < points0.size(); i++) { Point pt0 = (Point) points0.get(i); + if (pt0.isEmpty()) + continue; for (int j = 0; j < points1.size(); j++) { Point pt1 = (Point) points1.get(j); + if (pt1.isEmpty()) + continue; double dist = pt0.getCoordinate().distance(pt1.getCoordinate()); if (dist < minDistance) { minDistance = dist; @@ -368,6 +372,8 @@ private void computeMinDistanceLinesPoints(List lines, List points, LineString line = (LineString) lines.get(i); for (int j = 0; j < points.size(); j++) { Point pt = (Point) points.get(j); + if (pt.isEmpty()) + continue; computeMinDistance(line, pt, locGeom); if (minDistance <= terminateDistance) return; } diff --git a/modules/tests/src/test/resources/testxml/general/TestDistance.xml b/modules/tests/src/test/resources/testxml/general/TestDistance.xml index 4ce6d0fede..3835c919da 100644 --- a/modules/tests/src/test/resources/testxml/general/TestDistance.xml +++ b/modules/tests/src/test/resources/testxml/general/TestDistance.xml @@ -6,6 +6,7 @@ POINT(10 10) POINT EMPTY 0.0 + 0.0 @@ -13,6 +14,31 @@ POINT(10 10) POINT (10 0) 10.0 + 10.0 + + + + PP - point to multipoint + POINT(10 10) + MULTIPOINT ((10 0), (30 30)) + 10.0 + 10.0 + + + + PP - point to multipoint with empty element + POINT(10 10) + MULTIPOINT ((10 0), EMPTY) + 10.0 + 10.0 + + + + LL - line to empty line + LINESTRING (0 0, 0 10) + LINESTRING EMPTY + 0.0 + 0.0 @@ -20,6 +46,31 @@ LINESTRING (0 0, 0 10) LINESTRING (10 0, 10 10) 10.0 + 10.0 + + + + LL - line to multiline + LINESTRING (0 0, 0 10) + MULTILINESTRING ((10 0, 10 10), (50 50, 60 60)) + 10.0 + 10.0 + + + + LL - line to multiline with empty element + LINESTRING (0 0, 0 10) + MULTILINESTRING ((10 0, 10 10), EMPTY) + 10.0 + 10.0 + + + + PA - point to empty polygon + POINT (240 160) + POLYGON EMPTY + 0.0 + 0.0 @@ -27,6 +78,7 @@ POINT (240 160) POLYGON ((100 260, 340 180, 100 60, 180 160, 100 260)) 0.0 + 0.0 @@ -34,6 +86,7 @@ LINESTRING (40 300, 280 220, 60 160, 140 60) LINESTRING (140 360, 260 280, 240 120, 120 160) 0.0 + 0.0 @@ -41,6 +94,7 @@ POLYGON ((60 260, 260 180, 100 60, 60 160, 60 260)) POLYGON ((220 280, 120 160, 300 60, 360 220, 220 280)) 0.0 + 0.0 @@ -48,6 +102,7 @@ POLYGON ((100 320, 60 120, 240 180, 200 260, 100 320)) POLYGON ((420 320, 280 260, 400 100, 420 320)) 71.55417527999327 + 71.55417527999327 @@ -55,13 +110,23 @@ MULTIPOLYGON (((40 240, 160 320, 40 380, 40 240)), ((100 240, 240 60, 40 40, 100 240))) MULTIPOLYGON (((220 280, 120 160, 300 60, 360 220, 220 280)), ((240 380, 280 300, 420 340, 240 380))) 0.0 + 0.0 - mAmA - multipolygon with empty component + mAmA - multipolygon with empty element MULTIPOLYGON (EMPTY, ((98 200, 200 200, 200 99, 98 99, 98 200))) POLYGON ((300 200, 400 200, 400 100, 300 100, 300 200)) 100.0 + 100.0 + + + + GCGC - geometry collections with mixed dimensions + GEOMETRYCOLLECTION (LINESTRING (10 10, 50 10), POINT (90 10)) + GEOMETRYCOLLECTION (POLYGON ((90 20, 60 20, 60 50, 90 50, 90 20)), LINESTRING (10 50, 30 70)) + 10.0 + 10.0 From 6f2fce263863a7aac550f7128ce2266ee45d5184 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 31 Oct 2023 16:36:44 -0700 Subject: [PATCH 019/123] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index cff62ea8c0..93c47b0089 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -58,9 +58,10 @@ Distributions for older JTS versions can be obtained at the * Fix `LargestEmptyCircle` to handle polygonal obstacles (#988) * Make intersection computation more robust (#989) * Fix `VariableBuffer` to handle zero vertex buffer distances correctly (#997) -* Fix IncrementalDelaunayTriangulator to ensure triangulation boundary is convex (#1004) +* Fix `IncrementalDelaunayTriangulator` to ensure triangulation boundary is convex (#1004) * Fix OverlayNG Area Check heuristic for difference (#1005) -* Fix InteriorPointPoint to handle empty elements +* Fix `InteriorPointPoint` to handle empty elements +* Fix `DistanceOp` for empty elements (#1010) ### Performance Improvements From 0640925f1dc9f147890e74b02852e9bb4239942e Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 6 Nov 2023 15:29:10 -0800 Subject: [PATCH 020/123] Fix DouglaPeuckerSimplifier and TopologyPreservingSimplifier to handle ring endpoints (#1013) --- .../DouglasPeuckerLineSimplifier.java | 32 ++- .../simplify/DouglasPeuckerSimplifier.java | 3 +- .../jts/simplify/TaggedLineString.java | 31 +- .../simplify/TaggedLineStringSimplifier.java | 86 ++++-- .../TopologyPreservingSimplifier.java | 6 +- .../DouglasPeuckerSimplifierTest.java | 231 +++++++-------- .../TopologyPreservingSimplifierTest.java | 266 +++++++++--------- .../test/java/test/jts/GeometryTestCase.java | 4 + 8 files changed, 360 insertions(+), 299 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerLineSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerLineSimplifier.java index 05afe0be1b..d9a8ba04c9 100644 --- a/modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerLineSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerLineSimplifier.java @@ -13,6 +13,7 @@ package org.locationtech.jts.simplify; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateArrays; import org.locationtech.jts.geom.CoordinateList; import org.locationtech.jts.geom.LineSegment; @@ -24,16 +25,18 @@ */ class DouglasPeuckerLineSimplifier { - public static Coordinate[] simplify(Coordinate[] pts, double distanceTolerance) + public static Coordinate[] simplify(Coordinate[] pts, double distanceTolerance, boolean isPreserveEndpoint) { DouglasPeuckerLineSimplifier simp = new DouglasPeuckerLineSimplifier(pts); simp.setDistanceTolerance(distanceTolerance); + simp.setPreserveEndpoint(isPreserveEndpoint); return simp.simplify(); } private Coordinate[] pts; private boolean[] usePt; private double distanceTolerance; + private boolean isPreserveEndpoint = false; public DouglasPeuckerLineSimplifier(Coordinate[] pts) { @@ -50,6 +53,10 @@ public void setDistanceTolerance(double distanceTolerance) { this.distanceTolerance = distanceTolerance; } + private void setPreserveEndpoint(boolean isPreserveEndpoint) { + this.isPreserveEndpoint = isPreserveEndpoint; + } + public Coordinate[] simplify() { usePt = new boolean[pts.length]; @@ -57,12 +64,33 @@ public Coordinate[] simplify() usePt[i] = true; } simplifySection(0, pts.length - 1); + CoordinateList coordList = new CoordinateList(); for (int i = 0; i < pts.length; i++) { if (usePt[i]) coordList.add(new Coordinate(pts[i])); } - return coordList.toCoordinateArray(); + + if (! isPreserveEndpoint && CoordinateArrays.isRing(pts)) { + simplifyRingEndpoint(coordList); + } + + return coordList.toCoordinateArray(); + } + + private void simplifyRingEndpoint(CoordinateList pts) { + //-- avoid collapsing triangles + if (pts.size() < 4) + return; + //-- base segment for endpoint + seg.p0 = pts.get(1); + seg.p1 = pts.get(pts.size() - 2); + double distance = seg.distance(pts.get(0)); + if (distance <= distanceTolerance) { + pts.remove(0); + pts.remove(pts.size() - 1); + pts.closeRing(); + } } private LineSegment seg = new LineSegment(); diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifier.java index 6c0e3f358f..e14d0481c3 100644 --- a/modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifier.java @@ -135,13 +135,14 @@ public DPTransformer(boolean isEnsureValidTopology, double distanceTolerance) protected CoordinateSequence transformCoordinates(CoordinateSequence coords, Geometry parent) { + boolean isPreserveEndpoint = ! (parent instanceof LinearRing); Coordinate[] inputPts = coords.toCoordinateArray(); Coordinate[] newPts = null; if (inputPts.length == 0) { newPts = new Coordinate[0]; } else { - newPts = DouglasPeuckerLineSimplifier.simplify(inputPts, distanceTolerance); + newPts = DouglasPeuckerLineSimplifier.simplify(inputPts, distanceTolerance, isPreserveEndpoint); } return factory.getCoordinateSequenceFactory().create(newPts); } diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineString.java b/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineString.java index 1b5e67a7b6..e5b8fd82b0 100644 --- a/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineString.java +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineString.java @@ -34,17 +34,23 @@ class TaggedLineString private TaggedLineSegment[] segs; private List resultSegs = new ArrayList(); private int minimumSize; + private boolean isPreserveEndpoint = true; public TaggedLineString(LineString parentLine) { - this(parentLine, 2); + this(parentLine, 2, true); } - public TaggedLineString(LineString parentLine, int minimumSize) { + public TaggedLineString(LineString parentLine, int minimumSize, boolean isPreserveEndpoint) { this.parentLine = parentLine; this.minimumSize = minimumSize; + this.isPreserveEndpoint = isPreserveEndpoint; init(); } + public boolean isPreserveEndpoint() { + return isPreserveEndpoint; + } + public int getMinimumSize() { return minimumSize; } public LineString getParent() { return parentLine; } public Coordinate[] getParentCoordinates() { return parentLine.getCoordinates(); } @@ -58,6 +64,20 @@ public int getResultSize() public TaggedLineSegment getSegment(int i) { return segs[i]; } + /** + * Gets a segment of the result list. + * Negative indexes can be used to retrieve from the end of the list. + * @param i the segment index to retrieve + * @return the result segment + */ + public LineSegment getResultSegment(int i) { + int index = i; + if (i < 0) { + index = resultSegs.size() + i; + } + return (LineSegment) resultSegs.get(index); + } + private void init() { Coordinate[] pts = parentLine.getCoordinates(); @@ -98,5 +118,12 @@ private static Coordinate[] extractCoordinates(List segs) return pts; } + void removeRingEndpoint() + { + LineSegment firstSeg = (LineSegment) resultSegs.get(0); + LineSegment lastSeg = (LineSegment) resultSegs.get(resultSegs.size() - 1); + firstSeg.p0 = lastSeg.p0; + resultSegs.remove(resultSegs.size() - 1); + } } diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineStringSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineStringSimplifier.java index f5bc8b15c4..b481d4c173 100644 --- a/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineStringSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineStringSimplifier.java @@ -18,6 +18,7 @@ import org.locationtech.jts.algorithm.LineIntersector; import org.locationtech.jts.algorithm.RobustLineIntersector; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateArrays; import org.locationtech.jts.geom.LineSegment; /** @@ -66,13 +67,16 @@ void simplify(TaggedLineString line) this.line = line; linePts = line.getParentCoordinates(); simplifySection(0, linePts.length - 1, 0); + + if (! line.isPreserveEndpoint() && CoordinateArrays.isRing(linePts)) { + simplifyRingEndpoint(); + } } private void simplifySection(int i, int j, int depth) { depth += 1; - int[] sectionIndex = new int[2]; - if((i+1) == j) { + if ((i+1) == j) { LineSegment newSeg = line.getSegment(i); line.addToResult(newSeg); // leave this segment in the input index, for efficiency @@ -101,9 +105,7 @@ private void simplifySection(int i, int j, int depth) LineSegment candidateSeg = new LineSegment(); candidateSeg.p0 = linePts[i]; candidateSeg.p1 = linePts[j]; - sectionIndex[0] = i; - sectionIndex[1] = j; - if (hasBadIntersection(line, sectionIndex, candidateSeg)) { + if (hasBadIntersection(line, i, j, candidateSeg)) { isValidToSimplify = false; } @@ -116,6 +118,21 @@ private void simplifySection(int i, int j, int depth) simplifySection(furthestPtIndex, j, depth); } + private void simplifyRingEndpoint() + { + if (line.getResultSize() > line.getMinimumSize()) { + LineSegment firstSeg = line.getResultSegment(0); + LineSegment lastSeg = line.getResultSegment(-1); + + LineSegment simpSeg = new LineSegment(lastSeg.p0, firstSeg.p1); + //-- the excluded segments are the ones containing the endpoint + if (simpSeg.distance(firstSeg.p0) <= distanceTolerance + && ! hasBadIntersection(line, line.getSegments().length - 2, 0, simpSeg)) { + line.removeRingEndpoint(); + } + } + } + private int findFurthestPoint(Coordinate[] pts, int i, int j, double[] maxDistance) { LineSegment seg = new LineSegment(); @@ -158,12 +175,25 @@ private LineSegment flatten(int start, int end) return newSeg; } - private boolean hasBadIntersection(TaggedLineString parentLine, - int[] sectionIndex, + /** + * Tests if a flattening segment intersects a line + * (excluding a given section of segments). + * The excluded section is being replaced by the flattening segment, + * so there is no need to test it + * (and it may well intersect the segment). + * + * @param line + * @param excludeStart + * @param excludeEnd + * @param candidateSeg + * @return + */ + private boolean hasBadIntersection(TaggedLineString line, + int excludeStart, int excludeEnd, LineSegment candidateSeg) { if (hasBadOutputIntersection(candidateSeg)) return true; - if (hasBadInputIntersection(parentLine, sectionIndex, candidateSeg)) return true; + if (hasBadInputIntersection(line, excludeStart, excludeEnd, candidateSeg)) return true; return false; } @@ -179,16 +209,19 @@ private boolean hasBadOutputIntersection(LineSegment candidateSeg) return false; } - private boolean hasBadInputIntersection(TaggedLineString parentLine, - int[] sectionIndex, + private boolean hasBadInputIntersection(TaggedLineString line, + int excludeStart, int excludeEnd, LineSegment candidateSeg) { List querySegs = inputIndex.query(candidateSeg); for (Iterator i = querySegs.iterator(); i.hasNext(); ) { TaggedLineSegment querySeg = (TaggedLineSegment) i.next(); if (hasInvalidIntersection(querySeg, candidateSeg)) { - //-- don't fail if the segment is part of parent line - if (isInLineSection(parentLine, sectionIndex, querySeg)) + /** + * Ignore the intersection if the intersecting segment is part of the section being collapsed + * to the candidate segment + */ + if (isInLineSection(line, excludeStart, excludeEnd, querySeg)) continue; return true; } @@ -197,23 +230,36 @@ private boolean hasBadInputIntersection(TaggedLineString parentLine, } /** - * Tests whether a segment is in a section of a TaggedLineString - * @param line - * @param sectionIndex - * @param seg - * @return + * Tests whether a segment is in a section of a TaggedLineString. + * Sections may wrap around the endpoint of the line, + * to support ring endpoint simplification. + * This is indicated by excludedStart > excludedEnd + * + * @param line the TaggedLineString containing the section segments + * @param excludeStart the index of the first segment in the excluded section + * @param excludeEnd the index of the last segment in the excluded section + * @param seg the segment to test + * @return true if the test segment intersects some segment in the line not in the excluded section */ private static boolean isInLineSection( TaggedLineString line, - int[] sectionIndex, + int excludeStart, int excludeEnd, TaggedLineSegment seg) { - // not in this line + //-- test segment is not in this line if (seg.getParent() != line.getParent()) return false; int segIndex = seg.getIndex(); - if (segIndex >= sectionIndex[0] && segIndex < sectionIndex[1]) + if (excludeStart <= excludeEnd) { + //-- section is contiguous + if (segIndex >= excludeStart && segIndex < excludeEnd) + return true; + } + else { + //-- section wraps around the end of a ring + if (segIndex >= excludeStart || segIndex <= excludeEnd) return true; + } return false; } diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/TopologyPreservingSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/simplify/TopologyPreservingSimplifier.java index 33ea6e348b..eb154aafb2 100644 --- a/modules/core/src/main/java/org/locationtech/jts/simplify/TopologyPreservingSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/TopologyPreservingSimplifier.java @@ -45,6 +45,9 @@ * any intersecting line segments, this property * will be preserved in the output. *

+ * For polygonal geometries and LinearRings the endpoint will participate + * in simplification. For LineStrings the endpoints will not be unchanged. + *

* For all geometry types, the result will contain * enough vertices to ensure validity. For polygons * and closed linear geometries, the result will have at @@ -175,7 +178,8 @@ public void filter(Geometry geom) if (line.isEmpty()) return; int minSize = ((LineString) line).isClosed() ? 4 : 2; - TaggedLineString taggedLine = new TaggedLineString((LineString) line, minSize); + boolean isPreserveEndpoint = (line instanceof LinearRing) ? false : true; + TaggedLineString taggedLine = new TaggedLineString((LineString) line, minSize, isPreserveEndpoint); tps.linestringMap.put(line, taggedLine); } } diff --git a/modules/core/src/test/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifierTest.java index c260c1273b..df940a62df 100644 --- a/modules/core/src/test/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifierTest.java @@ -13,10 +13,7 @@ package org.locationtech.jts.simplify; import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.io.ParseException; -import org.locationtech.jts.io.WKTReader; -import junit.framework.TestCase; import test.jts.GeometryTestCase; @@ -34,125 +31,87 @@ public static void main(String[] args) { junit.textui.TestRunner.run(DouglasPeuckerSimplifierTest.class); } - public void testEmptyPolygon() throws Exception { - String geomStr = "POLYGON(EMPTY)"; - new GeometryOperationValidator( - DPSimplifierResult.getResult( - geomStr, - 1)) - .setExpectedResult(geomStr) - .test(); + public void testPoint() { + checkDPNoChange("POINT (10 10)", 1); } - - public void testPoint() throws Exception { - String geomStr = "POINT (10 10)"; - new GeometryOperationValidator( - DPSimplifierResult.getResult( - geomStr, - 1)) - .setExpectedResult(geomStr) - .test(); + + public void testPolygonEmpty() { + checkDPNoChange("POLYGON(EMPTY)", 1); } - - public void testPolygonNoReduction() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "POLYGON ((20 220, 40 220, 60 220, 80 220, 100 220, 120 220, 140 220, 140 180, 100 180, 60 180, 20 180, 20 220))", - 10.0)) - .test(); - } - public void testPolygonReductionWithSplit() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "POLYGON ((40 240, 160 241, 280 240, 280 160, 160 240, 40 140, 40 240))", - 10.0)) - .test(); - } - public void testPolygonReduction() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "POLYGON ((120 120, 121 121, 122 122, 220 120, 180 199, 160 200, 140 199, 120 120))", - 10.0)) - .test(); - } - public void testPolygonWithTouchingHole() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))", - 10.0)) - .setExpectedResult("POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))") - .test(); - } - public void testFlattishPolygon() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "POLYGON ((0 0, 50 0, 53 0, 55 0, 100 0, 70 1, 60 1, 50 1, 40 1, 0 0))", - 10.0)) - .setExpectedResult("POLYGON EMPTY") - .test(); - } - public void testTinySquare() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "POLYGON ((0 5, 5 5, 5 0, 0 0, 0 1, 0 5))", - 10.0)) - .test(); - } - public void testTinyHole() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "POLYGON ((10 10, 10 310, 370 310, 370 10, 10 10), (160 190, 180 190, 180 170, 160 190))", - 30.0)) - .testEmpty(false); - } - public void testTinyLineString() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "LINESTRING (0 5, 1 5, 2 5, 5 5)", - 10.0)) - .test(); - } - public void testMultiPoint() throws Exception { - String geomStr = "MULTIPOINT(80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120)"; - new GeometryOperationValidator( - TPSimplifierResult.getResult( - geomStr, - 10.0)) - .setExpectedResult(geomStr) - .test(); - } - public void testMultiLineString() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "MULTILINESTRING( (0 0, 50 0, 70 0, 80 0, 100 0), (0 0, 50 1, 60 1, 100 0) )", - 10.0)) - .test(); - } - public void testMultiLineStringWithEmpty() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "MULTILINESTRING( EMPTY, (0 0, 50 0, 70 0, 80 0, 100 0), (0 0, 50 1, 60 1, 100 0) )", - 10.0)) - .test(); - } - public void testMultiPolygonWithEmpty() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "MULTIPOLYGON (EMPTY, ((-36 91.5, 4.5 91.5, 4.5 57.5, -36 57.5, -36 91.5)), ((25.5 57.5, 61.5 57.5, 61.5 23.5, 25.5 23.5, 25.5 57.5)))", - 10.0)) - .test(); - } - public void testGeometryCollection() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "GEOMETRYCOLLECTION (" - + "MULTIPOINT (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120)," - + "POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200))," - + "LINESTRING (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120)" - + ")" - ,10.0)) - .test(); + public void testPolygonWithFlatVertices() { + checkDP("POLYGON ((20 220, 40 220, 60 220, 80 220, 100 220, 120 220, 140 220, 140 180, 100 180, 60 180, 20 180, 20 220))", + 10.0, + "POLYGON ((20 220, 140 220, 140 180, 20 180, 20 220))"); + } + + public void testPolygonReductionWithSplit() { + checkDP("POLYGON ((40 240, 160 241, 280 240, 280 160, 160 240, 40 140, 40 240))", + 1, + "MULTIPOLYGON (((40 240, 160 240, 40 140, 40 240)), ((160 240, 280 240, 280 160, 160 240)))"); + } + + public void testPolygonReduction() { + checkDP("POLYGON ((120 120, 121 121, 122 122, 220 120, 180 199, 160 200, 140 199, 120 120))", + 10.0, + "POLYGON ((120 120, 220 120, 180 199, 160 200, 140 199, 120 120)))"); + } + + public void testPolygonWithTouchingHole() { + checkDP("POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))", + 10, + "POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))"); + } + public void testFlattishPolygon() { + checkDP("POLYGON ((0 0, 50 0, 53 0, 55 0, 100 0, 70 1, 60 1, 50 1, 40 1, 0 0))", + 10, + "POLYGON EMPTY"); + } + + public void testTinySquare() { + checkDP("POLYGON ((0 5, 5 5, 5 0, 0 0, 0 1, 0 5))", + 10, + "POLYGON EMPTY"); + } + + public void testTinyHole() { + checkDP("POLYGON ((10 10, 10 310, 370 310, 370 10, 10 10), (160 190, 180 190, 180 170, 160 190))", + 30, + "POLYGON ((10 10, 10 310, 370 310, 370 10, 10 10))"); + } + + public void testTinyLineString() { + checkDP("LINESTRING (0 5, 1 5, 2 5, 5 5)", + 10, + "LINESTRING (0 5, 5 5)"); + } + + public void testMultiPoint() { + checkDPNoChange("MULTIPOINT(80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120)", + 10); + } + + public void testMultiLineString() { + checkDP("MULTILINESTRING((0 0, 50 0, 70 0, 80 0, 100 0), (0 0, 50 1, 60 1, 100 0) )", + 10, + "MULTILINESTRING ((0 0, 100 0), (0 0, 100 0))"); + } + + public void testMultiLineStringWithEmpty() { + checkDP("MULTILINESTRING( EMPTY, (0 0, 50 0, 70 0, 80 0, 100 0), (0 0, 50 1, 60 1, 100 0) )", + 10, + "MULTILINESTRING ((0 0, 100 0), (0 0, 100 0))"); + } + + public void testMultiPolygonWithEmpty() { + checkDP("MULTIPOLYGON (EMPTY, ((10 90, 10 10, 90 10, 50 60, 10 90)), ((70 90, 90 90, 90 70, 70 70, 70 90)))", + 10, + "MULTIPOLYGON (((10 90, 10 10, 90 10, 10 90)), ((70 90, 90 90, 90 70, 70 70, 70 90)))"); + } + + public void testGeometryCollection() { + checkDPNoChange("GEOMETRYCOLLECTION (MULTIPOINT (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120), POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200)), LINESTRING (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120))", + 10.0); } /** @@ -183,25 +142,31 @@ public void testPolygonCollapseRemoved() { ); } + // see https://trac.osgeo.org/geos/ticket/1064 + public void testPolygonRemoveFlatEndpoint() { + checkDP( + "POLYGON ((42 42, 0 42, 0 100, 42 100, 100 42, 42 42))", + 1, + "POLYGON ((100 42, 0 42, 0 100, 42 100, 100 42))" + ); + } + + public void testPolygonEndpointCollapse() { + checkDP( + "POLYGON ((5 2, 9 1, 1 1, 5 2))", + 1, + "POLYGON EMPTY" + ); + } + private void checkDP(String wkt, double tolerance, String wktExpected) { Geometry geom = read(wkt); Geometry result = DouglasPeuckerSimplifier.simplify(geom, tolerance); Geometry expected = read(wktExpected); checkEqual(expected, result); } -} - -class DPSimplifierResult -{ - private static WKTReader rdr = new WKTReader(); - - public static Geometry[] getResult(String wkt, double tolerance) - throws ParseException - { - Geometry[] ioGeom = new Geometry[2]; - ioGeom[0] = rdr.read(wkt); - ioGeom[1] = DouglasPeuckerSimplifier.simplify(ioGeom[0], tolerance); - //System.out.println(ioGeom[1]); - return ioGeom; + + private void checkDPNoChange(String wkt, double tolerance) { + checkDP(wkt, tolerance, wkt); } } diff --git a/modules/core/src/test/java/org/locationtech/jts/simplify/TopologyPreservingSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/simplify/TopologyPreservingSimplifierTest.java index c73f6b6649..06cdceca71 100644 --- a/modules/core/src/test/java/org/locationtech/jts/simplify/TopologyPreservingSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/simplify/TopologyPreservingSimplifierTest.java @@ -13,8 +13,6 @@ package org.locationtech.jts.simplify; import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.io.ParseException; -import org.locationtech.jts.io.WKTReader; import test.jts.GeometryTestCase; @@ -33,159 +31,91 @@ public static void main(String[] args) { junit.textui.TestRunner.run(TopologyPreservingSimplifierTest.class); } - public void testEmptyPolygon() throws Exception { - String geomStr = "POLYGON(EMPTY)"; - new GeometryOperationValidator( - TPSimplifierResult.getResult( - geomStr, - 1)) - .setExpectedResult(geomStr) - .test(); + public void testPoint() { + checkTPSNoChange("POINT (10 10)", 1); } - - public void testPoint() throws Exception { - String geomStr = "POINT (10 10)"; - new GeometryOperationValidator( - TPSimplifierResult.getResult( - geomStr, - 1)) - .setExpectedResult(geomStr) - .test(); - } - - - /** - * Test is from http://postgis.refractions.net/pipermail/postgis-users/2008-April/019327.html - * Exhibits the issue where simplified polygon shells can "jump" across - * holes, causing invalid topology. - * - * @throws Exception - */ - public void testMultiPolygonWithSmallComponents() throws Exception { - String geomStr = "MULTIPOLYGON(((13.73095 51.024734,13.7309323 51.0247668,13.7306959 51.0247959,13.7292724 51.0249742,13.7280216 51.0251252,13.7266598 51.0252998,13.7259617 51.0254072,13.7258854 51.0254201,13.7253253 51.0255144,13.725276 51.025492,13.724538 51.025631,13.7230288 51.0259021,13.7223529 51.0260273,13.7223299 51.0260863,13.7222292 51.026391,13.7220002 51.0273366,13.7217875 51.0282094,13.721746 51.028243,13.7217693 51.0282803,13.7215512 51.0291967,13.721513 51.029222,13.7215203 51.0292567,13.7212713 51.0295967,13.7222258 51.0299532,13.722234 51.03,13.7222931 51.0299823,13.7232514 51.0303187,13.7242514 51.0306715,13.724263 51.030714,13.7243024 51.0306951,13.7249934 51.0309315,13.7265097 51.0314552,13.7266116 51.0313952,13.7267988 51.0313334,13.7269952 51.0313243,13.72703 51.0314107,13.7271637 51.0313254,13.7272524 51.0313839,13.72739 51.031449,13.7276768 51.0313074,13.7283793 51.0309944,13.7296654 51.0304157,13.7297572 51.0303637,13.729845 51.0303139,13.7299557 51.0301763,13.7300964 51.0300176,13.730252 51.0298919,13.7304615 51.0297932,13.730668 51.0297363,13.730743 51.029783,13.7307859 51.0298398,13.7307094 51.0301388,13.730624 51.030263,13.7306955 51.0303267,13.7301182 51.0325594,13.7300528 51.0325663,13.7301114 51.0327342,13.7301645 51.0329094,13.7300035 51.0327693,13.7299669 51.0327351,13.7299445 51.0327211,13.7298934 51.032814,13.7298539 51.0328585,13.7297737 51.0328321,13.7288526 51.0325639,13.7288201 51.0324367,13.7284426 51.0324383,13.7276461 51.032179,13.7274569 51.0321976,13.7272787 51.0322421,13.7271265 51.0322903,13.7267034 51.0322495,13.7265364 51.0322161,13.7259018 51.0324269,13.7258649 51.03242,13.725733 51.0326646,13.7251933 51.0328876,13.7247918 51.0331374,13.7244439 51.0331106,13.7242967 51.0334273,13.7239131 51.0337529,13.7237035 51.0338511,13.7235429 51.033967,13.7233375 51.0339148,13.7232064 51.0339347,13.7231786 51.0339863,13.7228848 51.0340776,13.7224481 51.0341888,13.7220471 51.0342483,13.7217493 51.0343198,13.721552 51.0343861,13.7214718 51.0344095,13.7215108 51.034534,13.7205032 51.0349932,13.7197657 51.0352983,13.7195764 51.0352291,13.7195934 51.0352797,13.7182451 51.0359157,13.7181108 51.0359003,13.7181657 51.0359571,13.717622 51.0361956,13.7159749 51.0369683,13.7159057 51.0369284,13.7158604 51.0370288,13.7157161 51.0370124,13.7157523 51.0370733,13.7153708 51.0372801,13.7150274 51.0374899,13.7144074 51.0379192,13.7138287 51.0383899,13.7137514 51.0383857,13.7137492 51.0384566,13.7134249 51.0387269,13.7130179 51.0390385,13.7125791 51.0393343,13.7120736 51.039611,13.7115839 51.0398558,13.7112945 51.0399894,13.7114637 51.0402313,13.7123153 51.041449,13.7126333 51.0417033,13.713371 51.0421453,13.7138861 51.0424061,13.7142518 51.0425683,13.7164587 51.0435668,13.7167995 51.0437957,13.7170883 51.0439897,13.7190694 51.0451663,13.7196131 51.0458277,13.7197562 51.0461521,13.7198262 51.0464192,13.7198377 51.0467389,13.7205681 51.0455573,13.7210009 51.0450379,13.7214987 51.0445401,13.7220306 51.0442859,13.7227215 51.0439558,13.7237962 51.0434514,13.723979 51.0435278,13.7241448 51.0435041,13.7241052 51.0436042,13.7247987 51.0438896,13.7250186 51.0439093,13.7250579 51.0440386,13.7257225 51.0443545,13.7259312 51.0443456,13.725955 51.0443813,13.7260235 51.0443873,13.7260682 51.0445303,13.7282191 51.0455848,13.7290532 51.045927,13.7292643 51.0458591,13.7292228 51.0459969,13.729706 51.0461854,13.7303185 51.046393,13.7309107 51.0465601,13.731546 51.0466841,13.7321939 51.0467752,13.7332896 51.0468999,13.7333733 51.0469094,13.7334778 51.0468127,13.7335706 51.0469078,13.733651 51.0470684,13.7338458 51.0471508,13.7346109 51.0472333,13.7346367 51.0471474,13.7346922 51.0470697,13.7346666 51.0470056,13.7346564 51.0468714,13.7345552 51.0467095,13.7336001 51.0465496,13.733427 51.046454,13.7335317 51.0464255,13.7347225 51.0465948,13.7348421 51.0466562,13.7349123 51.0466203,13.736811 51.0468537,13.7382043 51.0469796,13.7383487 51.0469803,13.7394909 51.0469005,13.7400899 51.0467949,13.7405051 51.0464739,13.7408331 51.0462204,13.7412027 51.0463256,13.741053 51.0466451,13.7407291 51.0469007,13.7405095 51.0469726,13.7400888 51.0470337,13.7393051 51.0471049,13.7393014 51.0472015,13.7393088 51.0473019,13.7395556 51.0473056,13.7404944 51.0472245,13.740932 51.0470192,13.7414421 51.0465652,13.7414893 51.0465576,13.7416494 51.0464916,13.7416003 51.0466074,13.7416246 51.04663,13.741668 51.0466443,13.7417272 51.0467159,13.7417503 51.0466716,13.7423587 51.0468732,13.7426958 51.0470246,13.7429143 51.0471813,13.74318 51.04726,13.7430363 51.0472995,13.7433021 51.047588,13.7434678 51.0475916,13.7433805 51.0477019,13.7436362 51.0479981,13.7446308 51.0491622,13.7447961 51.0491827,13.744722 51.0492509,13.7448536 51.0494078,13.745056 51.0494766,13.7450313 51.0496901,13.7453573 51.0500052,13.7465317 51.0512807,13.7466999 51.0513722,13.746638 51.0514149,13.7468683 51.0516781,13.7470071 51.051777,13.7469985 51.0518746,13.7470732 51.0519866,13.7471316 51.0520528,13.7472989 51.0523089,13.7472368 51.0523858,13.7473063 51.0524932,13.7473468 51.0527412,13.7473392 51.0531614,13.7472987 51.0533157,13.7473919 51.0534224,13.7472684 51.0534549,13.7472134 51.0536926,13.7472913 51.0537784,13.7473216 51.053725,13.7474649 51.0537575,13.7474492 51.053833,13.7475625 51.0537839,13.7497379 51.0544435,13.7515333 51.0551019,13.7527693 51.0555438,13.7549766 51.0564993,13.7550622 51.0565364,13.755105 51.0566612,13.7552745 51.0566237,13.7558661 51.0560648,13.7559318 51.0560101,13.755908 51.055897,13.7559252 51.0558292,13.7559566 51.0557055,13.7564494 51.0551377,13.7564124 51.0550457,13.7573213 51.0539813,13.7575007 51.0539933,13.757856 51.0540047,13.7580394 51.054028,13.7580896 51.053984,13.7580949 51.0539463,13.7579963 51.0538534,13.7581294 51.0537147,13.7582346 51.0535957,13.758354 51.053433,13.758363 51.053392,13.7583656 51.0533457,13.758359 51.0532095,13.7583338 51.0530937,13.7582902 51.0529647,13.7580365 51.0522637,13.7577683 51.051463,13.7573182 51.0501993,13.7571595 51.0497164,13.7567579 51.0490095,13.7563383 51.0482979,13.7557757 51.0473383,13.7557095 51.0472522,13.7555771 51.0471199,13.7554448 51.0470471,13.7548596 51.0462612,13.7547097 51.046054,13.7549127 51.0460086,13.7548633 51.0459174,13.7548127 51.0458413,13.7547176 51.0457237,13.7538293 51.0449222,13.7530218 51.0441346,13.7526711 51.0437838,13.752446 51.0435522,13.7522297 51.0433547,13.751704 51.042833,13.7513058 51.0424448,13.7505766 51.0417281,13.7499967 51.0411283,13.7497695 51.0408943,13.7493849 51.0405205,13.7486222 51.0397896,13.7478209 51.0390261,13.7477474 51.0389532,13.7477041 51.0389189,13.7476277 51.0388729,13.7475781 51.0388513,13.7472699 51.038726,13.747131 51.0386506,13.7469329 51.0385052,13.7468562 51.0384284,13.7466683 51.0383483,13.7467998 51.038236,13.7473841 51.0380129,13.747838 51.0378277,13.7481801 51.0376558,13.7489728 51.0370285,13.7491313 51.0368016,13.7492665 51.0363477,13.7493166 51.0359389,13.7492966 51.0358087,13.7493888 51.0356942,13.7492867 51.0357016,13.7492855 51.0354359,13.7492829 51.034867,13.7492723 51.0348311,13.7492455 51.0347398,13.7493034 51.0346612,13.7491987 51.0346142,13.748866 51.034723,13.748791 51.034201,13.748335 51.034159,13.748294 51.034034,13.748205 51.033764,13.7488691 51.0333037,13.748962 51.033245,13.7486777 51.0332252,13.7483008 51.032683,13.7484397 51.0324582,13.7469913 51.0327817,13.7466998 51.0326205,13.7459997 51.0314852,13.7460996 51.0313569,13.745967 51.0314864,13.7449355 51.0317377,13.7447301 51.0316513,13.7446705 51.0318463,13.7420262 51.0323659,13.7419131 51.0322884,13.7418636 51.0322552,13.7416501 51.0321425,13.7415567 51.0317708,13.7414972 51.0314666,13.741484 51.0311492,13.741923 51.031003,13.7418649 51.030884,13.74209 51.0304134,13.7422077 51.0300143,13.7421975 51.0299222,13.742286 51.029835,13.7421463 51.0297533,13.7420951 51.0296254,13.7415933 51.0288452,13.7414906 51.0286855,13.7414437 51.0286127,13.7413482 51.0284642,13.7410545 51.0280777,13.7407158 51.0277229,13.7401513 51.0273842,13.7392803 51.0270293,13.7382744 51.0267844,13.737321 51.0267454,13.7365929 51.0267541,13.736556 51.026812,13.7364715 51.026754,13.7357088 51.0268017,13.7353967 51.02678,13.73534 51.02685,13.7352667 51.0267757,13.734907 51.0267324,13.734824 51.02679,13.7347684 51.0267064,13.7342093 51.0266674,13.73409 51.026725,13.7340359 51.0266283,13.7335072 51.0265633,13.733407 51.02663,13.7333208 51.0265373,13.7317087 51.0263813,13.7317173 51.0263119,13.73167 51.026241,13.7317563 51.0261602,13.7318473 51.0258395,13.7318647 51.0254971,13.73183 51.0253281,13.7317736 51.0252414,13.731663 51.025181,13.7316826 51.0251114,13.7310803 51.0247604,13.73095 51.024734)),((13.7368533 51.0470386,13.7368426 51.0471226,13.7368067 51.0472669,13.7368255 51.0473828,13.7369099 51.0474154,13.7376695 51.0474677,13.7382756 51.0474245,13.738513 51.0474297,13.7386105 51.0474065,13.738705 51.0473737,13.7385856 51.0473757,13.7385618 51.0473751,13.7385263 51.0473743,13.7384706 51.0473744,13.7383071 51.0473734,13.7383822 51.0473564,13.7390821 51.047287,13.7390933 51.047209,13.7390933 51.0471421,13.7368533 51.0470386)),((13.7367293 51.0470057,13.7346615 51.0466892,13.7347551 51.0468411,13.7347754 51.0470359,13.7347106 51.0471899,13.7356421 51.0472919,13.7366963 51.0474074,13.736705 51.047249,13.7367293 51.0470057)))"; - boolean isPassed = new GeometryOperationValidator( - TPSimplifierResult.getResult( - geomStr, - 0.0057)) - .isAllTestsPassed(); - assertTrue(! isPassed); + + public void testPolygonEmpty() { + checkTPSNoChange("POLYGON(EMPTY)", 1); } - /** - * Test is from http://lists.jump-project.org/pipermail/jts-devel/2008-February/002350.html - * @throws Exception - */ - public void testPolygonWithSpike() throws Exception { - String geomStr = "POLYGON ((3312459.605 6646878.353, 3312460.524 6646875.969, 3312459.427 6646878.421, 3312460.014 6646886.391, 3312465.889 6646887.398, 3312470.827 6646884.839, 3312475.4 6646878.027, 3312477.289 6646871.694, 3312472.748 6646869.547, 3312468.253 6646874.01, 3312463.52 6646875.779, 3312459.605 6646878.353))"; - new GeometryOperationValidator( - TPSimplifierResult.getResult( - geomStr, - 2.0)) - .test(); + + public void testPolygonFlatVertices() throws Exception { + checkTPS("POLYGON ((20 220, 40 220, 60 220, 80 220, 100 220, 120 220, 140 220, 140 180, 100 180, 60 180, 20 180, 20 220))", + 10, + "POLYGON ((20 220, 140 220, 140 180, 20 180, 20 220))"); } + public void testPolygonNoReduction() throws Exception { - new GeometryOperationValidator( - TPSimplifierResult.getResult( - "POLYGON ((20 220, 40 220, 60 220, 80 220, 100 220, 120 220, 140 220, 140 180, 100 180, 60 180, 20 180, 20 220))", - 10.0)) - .test(); + checkTPSNoChange("POLYGON ((20 220, 140 220, 140 180, 20 180, 20 220))", + 10); } + public void testPolygonNoReductionWithConflicts() throws Exception { - new GeometryOperationValidator( - TPSimplifierResult.getResult( - "POLYGON ((40 240, 160 241, 280 240, 280 160, 160 240, 40 140, 40 240))", - 10.0)) - .test(); + checkTPSNoChange("POLYGON ((40 240, 160 241, 280 240, 280 160, 160 240, 40 140, 40 240))", + 10); } + public void testPolygonWithTouchingHole() throws Exception { - new GeometryOperationValidator( - TPSimplifierResult.getResult( - "POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))", - 10.0)) - .setExpectedResult("POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))") - .test(); + checkTPS("POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))", + 10, + "POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))"); } + public void testFlattishPolygon() throws Exception { - new GeometryOperationValidator( - TPSimplifierResult.getResult( - "POLYGON ((0 0, 50 0, 53 0, 55 0, 100 0, 70 1, 60 1, 50 1, 40 1, 0 0))", - 10.0)) - .test(); + checkTPS("POLYGON ((0 0, 50 0, 53 0, 55 0, 100 0, 70 1, 60 1, 50 1, 40 1, 0 0))", + 10, + "POLYGON ((0 0, 50 0, 100 0, 70 1, 0 0))"); } + public void testPolygonWithFlattishHole() throws Exception { - String geomStr = "POLYGON ((0 0, 0 200, 200 200, 200 0, 0 0), (140 40, 90 95, 40 160, 95 100, 140 40))"; - new GeometryOperationValidator( - TPSimplifierResult.getResult( - geomStr, - 20.0)) - .setExpectedResult(geomStr) - .test(); + checkTPS("POLYGON ((0 0, 0 200, 200 200, 200 0, 0 0), (140 40, 90 95, 40 160, 95 100, 140 40))", + 20, + "POLYGON ((0 0, 0 200, 200 200, 200 0, 0 0), (140 40, 90 95, 40 160, 95 100, 140 40))"); } + public void testTinySquare() throws Exception { - new GeometryOperationValidator( - TPSimplifierResult.getResult( - "POLYGON ((0 5, 5 5, 5 0, 0 0, 0 1, 0 5))", - 10.0)) - .test(); + checkTPS("POLYGON ((0 5, 5 5, 5 0, 0 0, 0 1, 0 5))", + 10, + "POLYGON ((0 0, 5 5, 5 0, 0 0))"); } + public void testTinyLineString() throws Exception { - new GeometryOperationValidator( - TPSimplifierResult.getResult( - "LINESTRING (0 5, 1 5, 2 5, 5 5)", - 10.0)) - .test(); + checkTPS("LINESTRING (0 5, 1 5, 2 5, 5 5)", + 10, + "LINESTRING (0 5, 5 5)"); } public void testTinyClosedLineString() throws Exception { - String geomStr = "LINESTRING (0 0, 5 0, 5 5, 0 0)"; - new GeometryOperationValidator( - TPSimplifierResult.getResult( - geomStr, - 10)) - .setExpectedResult(geomStr) - .test(); + checkTPSNoChange("LINESTRING (0 0, 5 0, 5 5, 0 0)", + 10); } public void testMultiPoint() throws Exception { - String geomStr = "MULTIPOINT(80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120)"; - new GeometryOperationValidator( - TPSimplifierResult.getResult( - geomStr, - 10.0)) - .setExpectedResult(geomStr) - .test(); + checkTPSNoChange("MULTIPOINT(80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120)", + 10); } public void testMultiLineString() throws Exception { - new GeometryOperationValidator( - TPSimplifierResult.getResult( - "MULTILINESTRING( (0 0, 50 0, 70 0, 80 0, 100 0), (0 0, 50 1, 60 1, 100 0) )", - 10.0)) - .test(); + checkTPS("MULTILINESTRING( (0 0, 50 0, 70 0, 80 0, 100 0), (0 0, 50 1, 60 1, 100 0) )", + 10, + "MULTILINESTRING ((0 0, 100 0), (0 0, 50 1, 100 0))"); } + public void testMultiLineStringWithEmpty() throws Exception { - new GeometryOperationValidator( - TPSimplifierResult.getResult( - "MULTILINESTRING(EMPTY, (0 0, 50 0, 70 0, 80 0, 100 0), (0 0, 50 1, 60 1, 100 0) )", - 10.0)) - .test(); + checkTPS("MULTILINESTRING(EMPTY, (0 0, 50 0, 70 0, 80 0, 100 0), (0 0, 50 1, 60 1, 100 0) )", + 10, + "MULTILINESTRING ((0 0, 100 0), (0 0, 50 1, 100 0))"); } + public void testMultiPolygonWithEmpty() throws Exception { - new GeometryOperationValidator( - TPSimplifierResult.getResult( - "MULTIPOLYGON (EMPTY, ((-36 91.5, 4.5 91.5, 4.5 57.5, -36 57.5, -36 91.5)), ((25.5 57.5, 61.5 57.5, 61.5 23.5, 25.5 23.5, 25.5 57.5)))", - 10.0)) - .test(); - } - public void testGeometryCollection() throws Exception { - new GeometryOperationValidator( - TPSimplifierResult.getResult( - "GEOMETRYCOLLECTION (" - + "MULTIPOINT (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120)," - + "POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200))," - + "LINESTRING (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120)" - + ")" - ,10.0)) - .test(); + checkTPS("MULTIPOLYGON (EMPTY, ((10 90, 10 10, 90 10, 50 60, 10 90)), ((70 90, 90 90, 90 70, 70 70, 70 90)))", + 10, + "MULTIPOLYGON (((10 90, 10 10, 90 10, 50 60, 10 90)), ((70 90, 90 90, 90 70, 70 70, 70 90)))"); + } + + public void testGeometryCollection() { + checkTPSNoChange("GEOMETRYCOLLECTION (MULTIPOINT (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120), POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200)), LINESTRING (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120))", + 10); } public void testNoCollapse_mL() throws Exception { @@ -204,25 +134,81 @@ public void testNoCollapseMany_mL() throws Exception { ); } + public void testNoCollapseSmallSquare() throws Exception { + checkTPS( + "POLYGON ((0 5, 5 5, 5 0, 0 0, 0 1, 0 5))", + 100, + "POLYGON ((0 0, 5 5, 5 0, 0 0))" + ); + } + + public void testPolygonRemoveEndpoint() throws Exception { + checkTPS( + "POLYGON ((220 180, 261 175, 380 220, 300 40, 140 30, 30 220, 176 176, 220 180))", + 40, + "POLYGON ((30 220, 380 220, 300 40, 140 30, 30 220))" + ); + } + + public void testLinearRingRemoveEndpoint() throws Exception { + checkTPS( + "LINEARRING (220 180, 261 175, 380 220, 300 40, 140 30, 30 220, 176 176, 220 180)", + 40, + "LINEARRING (30 220, 380 220, 300 40, 140 30, 30 220)" + ); + } + + public void testPolygonKeepEndpointWithCross() throws Exception { + checkTPS( + "POLYGON ((50 52, 60 50, 90 60, 90 10, 10 10, 10 90, 60 90, 50 55, 40 80, 20 60, 40 50, 50 52))", + 10, + "POLYGON ((20 60, 50 52, 90 60, 90 10, 10 10, 10 90, 60 90, 50 55, 40 80, 20 60))" + ); + } + + // see https://trac.osgeo.org/geos/ticket/1064 + public void testPolygonRemoveFlatEndpoint() throws Exception { + checkTPS( + "POLYGON ((42 42, 0 42, 0 100, 42 100, 100 42, 42 42))", + 1, + "POLYGON ((100 42, 0 42, 0 100, 42 100, 100 42))" + ); + } + + /** + * Test is from http://postgis.refractions.net/pipermail/postgis-users/2008-April/019327.html + * Exhibits the issue where simplified polygon shells can "jump" across + * holes, causing invalid topology. + * + * @throws Exception + */ + public void testMultiPolygonWithSmallComponents() throws Exception { + checkTPS("MULTIPOLYGON(((13.73095 51.024734,13.7309323 51.0247668,13.7306959 51.0247959,13.7292724 51.0249742,13.7280216 51.0251252,13.7266598 51.0252998,13.7259617 51.0254072,13.7258854 51.0254201,13.7253253 51.0255144,13.725276 51.025492,13.724538 51.025631,13.7230288 51.0259021,13.7223529 51.0260273,13.7223299 51.0260863,13.7222292 51.026391,13.7220002 51.0273366,13.7217875 51.0282094,13.721746 51.028243,13.7217693 51.0282803,13.7215512 51.0291967,13.721513 51.029222,13.7215203 51.0292567,13.7212713 51.0295967,13.7222258 51.0299532,13.722234 51.03,13.7222931 51.0299823,13.7232514 51.0303187,13.7242514 51.0306715,13.724263 51.030714,13.7243024 51.0306951,13.7249934 51.0309315,13.7265097 51.0314552,13.7266116 51.0313952,13.7267988 51.0313334,13.7269952 51.0313243,13.72703 51.0314107,13.7271637 51.0313254,13.7272524 51.0313839,13.72739 51.031449,13.7276768 51.0313074,13.7283793 51.0309944,13.7296654 51.0304157,13.7297572 51.0303637,13.729845 51.0303139,13.7299557 51.0301763,13.7300964 51.0300176,13.730252 51.0298919,13.7304615 51.0297932,13.730668 51.0297363,13.730743 51.029783,13.7307859 51.0298398,13.7307094 51.0301388,13.730624 51.030263,13.7306955 51.0303267,13.7301182 51.0325594,13.7300528 51.0325663,13.7301114 51.0327342,13.7301645 51.0329094,13.7300035 51.0327693,13.7299669 51.0327351,13.7299445 51.0327211,13.7298934 51.032814,13.7298539 51.0328585,13.7297737 51.0328321,13.7288526 51.0325639,13.7288201 51.0324367,13.7284426 51.0324383,13.7276461 51.032179,13.7274569 51.0321976,13.7272787 51.0322421,13.7271265 51.0322903,13.7267034 51.0322495,13.7265364 51.0322161,13.7259018 51.0324269,13.7258649 51.03242,13.725733 51.0326646,13.7251933 51.0328876,13.7247918 51.0331374,13.7244439 51.0331106,13.7242967 51.0334273,13.7239131 51.0337529,13.7237035 51.0338511,13.7235429 51.033967,13.7233375 51.0339148,13.7232064 51.0339347,13.7231786 51.0339863,13.7228848 51.0340776,13.7224481 51.0341888,13.7220471 51.0342483,13.7217493 51.0343198,13.721552 51.0343861,13.7214718 51.0344095,13.7215108 51.034534,13.7205032 51.0349932,13.7197657 51.0352983,13.7195764 51.0352291,13.7195934 51.0352797,13.7182451 51.0359157,13.7181108 51.0359003,13.7181657 51.0359571,13.717622 51.0361956,13.7159749 51.0369683,13.7159057 51.0369284,13.7158604 51.0370288,13.7157161 51.0370124,13.7157523 51.0370733,13.7153708 51.0372801,13.7150274 51.0374899,13.7144074 51.0379192,13.7138287 51.0383899,13.7137514 51.0383857,13.7137492 51.0384566,13.7134249 51.0387269,13.7130179 51.0390385,13.7125791 51.0393343,13.7120736 51.039611,13.7115839 51.0398558,13.7112945 51.0399894,13.7114637 51.0402313,13.7123153 51.041449,13.7126333 51.0417033,13.713371 51.0421453,13.7138861 51.0424061,13.7142518 51.0425683,13.7164587 51.0435668,13.7167995 51.0437957,13.7170883 51.0439897,13.7190694 51.0451663,13.7196131 51.0458277,13.7197562 51.0461521,13.7198262 51.0464192,13.7198377 51.0467389,13.7205681 51.0455573,13.7210009 51.0450379,13.7214987 51.0445401,13.7220306 51.0442859,13.7227215 51.0439558,13.7237962 51.0434514,13.723979 51.0435278,13.7241448 51.0435041,13.7241052 51.0436042,13.7247987 51.0438896,13.7250186 51.0439093,13.7250579 51.0440386,13.7257225 51.0443545,13.7259312 51.0443456,13.725955 51.0443813,13.7260235 51.0443873,13.7260682 51.0445303,13.7282191 51.0455848,13.7290532 51.045927,13.7292643 51.0458591,13.7292228 51.0459969,13.729706 51.0461854,13.7303185 51.046393,13.7309107 51.0465601,13.731546 51.0466841,13.7321939 51.0467752,13.7332896 51.0468999,13.7333733 51.0469094,13.7334778 51.0468127,13.7335706 51.0469078,13.733651 51.0470684,13.7338458 51.0471508,13.7346109 51.0472333,13.7346367 51.0471474,13.7346922 51.0470697,13.7346666 51.0470056,13.7346564 51.0468714,13.7345552 51.0467095,13.7336001 51.0465496,13.733427 51.046454,13.7335317 51.0464255,13.7347225 51.0465948,13.7348421 51.0466562,13.7349123 51.0466203,13.736811 51.0468537,13.7382043 51.0469796,13.7383487 51.0469803,13.7394909 51.0469005,13.7400899 51.0467949,13.7405051 51.0464739,13.7408331 51.0462204,13.7412027 51.0463256,13.741053 51.0466451,13.7407291 51.0469007,13.7405095 51.0469726,13.7400888 51.0470337,13.7393051 51.0471049,13.7393014 51.0472015,13.7393088 51.0473019,13.7395556 51.0473056,13.7404944 51.0472245,13.740932 51.0470192,13.7414421 51.0465652,13.7414893 51.0465576,13.7416494 51.0464916,13.7416003 51.0466074,13.7416246 51.04663,13.741668 51.0466443,13.7417272 51.0467159,13.7417503 51.0466716,13.7423587 51.0468732,13.7426958 51.0470246,13.7429143 51.0471813,13.74318 51.04726,13.7430363 51.0472995,13.7433021 51.047588,13.7434678 51.0475916,13.7433805 51.0477019,13.7436362 51.0479981,13.7446308 51.0491622,13.7447961 51.0491827,13.744722 51.0492509,13.7448536 51.0494078,13.745056 51.0494766,13.7450313 51.0496901,13.7453573 51.0500052,13.7465317 51.0512807,13.7466999 51.0513722,13.746638 51.0514149,13.7468683 51.0516781,13.7470071 51.051777,13.7469985 51.0518746,13.7470732 51.0519866,13.7471316 51.0520528,13.7472989 51.0523089,13.7472368 51.0523858,13.7473063 51.0524932,13.7473468 51.0527412,13.7473392 51.0531614,13.7472987 51.0533157,13.7473919 51.0534224,13.7472684 51.0534549,13.7472134 51.0536926,13.7472913 51.0537784,13.7473216 51.053725,13.7474649 51.0537575,13.7474492 51.053833,13.7475625 51.0537839,13.7497379 51.0544435,13.7515333 51.0551019,13.7527693 51.0555438,13.7549766 51.0564993,13.7550622 51.0565364,13.755105 51.0566612,13.7552745 51.0566237,13.7558661 51.0560648,13.7559318 51.0560101,13.755908 51.055897,13.7559252 51.0558292,13.7559566 51.0557055,13.7564494 51.0551377,13.7564124 51.0550457,13.7573213 51.0539813,13.7575007 51.0539933,13.757856 51.0540047,13.7580394 51.054028,13.7580896 51.053984,13.7580949 51.0539463,13.7579963 51.0538534,13.7581294 51.0537147,13.7582346 51.0535957,13.758354 51.053433,13.758363 51.053392,13.7583656 51.0533457,13.758359 51.0532095,13.7583338 51.0530937,13.7582902 51.0529647,13.7580365 51.0522637,13.7577683 51.051463,13.7573182 51.0501993,13.7571595 51.0497164,13.7567579 51.0490095,13.7563383 51.0482979,13.7557757 51.0473383,13.7557095 51.0472522,13.7555771 51.0471199,13.7554448 51.0470471,13.7548596 51.0462612,13.7547097 51.046054,13.7549127 51.0460086,13.7548633 51.0459174,13.7548127 51.0458413,13.7547176 51.0457237,13.7538293 51.0449222,13.7530218 51.0441346,13.7526711 51.0437838,13.752446 51.0435522,13.7522297 51.0433547,13.751704 51.042833,13.7513058 51.0424448,13.7505766 51.0417281,13.7499967 51.0411283,13.7497695 51.0408943,13.7493849 51.0405205,13.7486222 51.0397896,13.7478209 51.0390261,13.7477474 51.0389532,13.7477041 51.0389189,13.7476277 51.0388729,13.7475781 51.0388513,13.7472699 51.038726,13.747131 51.0386506,13.7469329 51.0385052,13.7468562 51.0384284,13.7466683 51.0383483,13.7467998 51.038236,13.7473841 51.0380129,13.747838 51.0378277,13.7481801 51.0376558,13.7489728 51.0370285,13.7491313 51.0368016,13.7492665 51.0363477,13.7493166 51.0359389,13.7492966 51.0358087,13.7493888 51.0356942,13.7492867 51.0357016,13.7492855 51.0354359,13.7492829 51.034867,13.7492723 51.0348311,13.7492455 51.0347398,13.7493034 51.0346612,13.7491987 51.0346142,13.748866 51.034723,13.748791 51.034201,13.748335 51.034159,13.748294 51.034034,13.748205 51.033764,13.7488691 51.0333037,13.748962 51.033245,13.7486777 51.0332252,13.7483008 51.032683,13.7484397 51.0324582,13.7469913 51.0327817,13.7466998 51.0326205,13.7459997 51.0314852,13.7460996 51.0313569,13.745967 51.0314864,13.7449355 51.0317377,13.7447301 51.0316513,13.7446705 51.0318463,13.7420262 51.0323659,13.7419131 51.0322884,13.7418636 51.0322552,13.7416501 51.0321425,13.7415567 51.0317708,13.7414972 51.0314666,13.741484 51.0311492,13.741923 51.031003,13.7418649 51.030884,13.74209 51.0304134,13.7422077 51.0300143,13.7421975 51.0299222,13.742286 51.029835,13.7421463 51.0297533,13.7420951 51.0296254,13.7415933 51.0288452,13.7414906 51.0286855,13.7414437 51.0286127,13.7413482 51.0284642,13.7410545 51.0280777,13.7407158 51.0277229,13.7401513 51.0273842,13.7392803 51.0270293,13.7382744 51.0267844,13.737321 51.0267454,13.7365929 51.0267541,13.736556 51.026812,13.7364715 51.026754,13.7357088 51.0268017,13.7353967 51.02678,13.73534 51.02685,13.7352667 51.0267757,13.734907 51.0267324,13.734824 51.02679,13.7347684 51.0267064,13.7342093 51.0266674,13.73409 51.026725,13.7340359 51.0266283,13.7335072 51.0265633,13.733407 51.02663,13.7333208 51.0265373,13.7317087 51.0263813,13.7317173 51.0263119,13.73167 51.026241,13.7317563 51.0261602,13.7318473 51.0258395,13.7318647 51.0254971,13.73183 51.0253281,13.7317736 51.0252414,13.731663 51.025181,13.7316826 51.0251114,13.7310803 51.0247604,13.73095 51.024734)),((13.7368533 51.0470386,13.7368426 51.0471226,13.7368067 51.0472669,13.7368255 51.0473828,13.7369099 51.0474154,13.7376695 51.0474677,13.7382756 51.0474245,13.738513 51.0474297,13.7386105 51.0474065,13.738705 51.0473737,13.7385856 51.0473757,13.7385618 51.0473751,13.7385263 51.0473743,13.7384706 51.0473744,13.7383071 51.0473734,13.7383822 51.0473564,13.7390821 51.047287,13.7390933 51.047209,13.7390933 51.0471421,13.7368533 51.0470386)),((13.7367293 51.0470057,13.7346615 51.0466892,13.7347551 51.0468411,13.7347754 51.0470359,13.7347106 51.0471899,13.7356421 51.0472919,13.7366963 51.0474074,13.736705 51.047249,13.7367293 51.0470057)))", + 0.0057, + "MULTIPOLYGON (((13.73095 51.024734, 13.7123153 51.041449, 13.7552745 51.0566237, 13.7484397 51.0324582, 13.73095 51.024734)), ((13.7390933 51.0471421, 13.7369099 51.0474154, 13.7390933 51.047209, 13.7390933 51.0471421)), ((13.7367293 51.0470057, 13.7346615 51.0466892, 13.7347106 51.0471899, 13.7367293 51.0470057)))"); + } + + /** + * Test is from http://lists.jump-project.org/pipermail/jts-devel/2008-February/002350.html + * @throws Exception + */ + public void testPolygonWithSpike() throws Exception { + checkTPS("POLYGON ((3312459.605 6646878.353, 3312460.524 6646875.969, 3312459.427 6646878.421, 3312460.014 6646886.391, 3312465.889 6646887.398, 3312470.827 6646884.839, 3312475.4 6646878.027, 3312477.289 6646871.694, 3312472.748 6646869.547, 3312468.253 6646874.01, 3312463.52 6646875.779, 3312459.605 6646878.353))", + 2, + "POLYGON ((3312459.605 6646878.353, 3312460.524 6646875.969, 3312459.427 6646878.421, 3312460.014 6646886.391, 3312465.889 6646887.398, 3312470.827 6646884.839, 3312477.289 6646871.694, 3312472.748 6646869.547, 3312459.605 6646878.353))"); + } + private void checkTPS(String wkt, double tolerance, String wktExpected) { Geometry geom = read(wkt); Geometry actual = TopologyPreservingSimplifier.simplify(geom, tolerance); Geometry expected = read(wktExpected); + //TODO: add this once the "skipping over rings" problem is fixed + //checkValid(actual); checkEqual(expected, actual); } -} - -class TPSimplifierResult -{ - private static WKTReader rdr = new WKTReader(); - - public static Geometry[] getResult(String wkt, double tolerance) - throws ParseException - { - Geometry[] ioGeom = new Geometry[2]; - ioGeom[0] = rdr.read(wkt); - ioGeom[1] = TopologyPreservingSimplifier.simplify(ioGeom[0], tolerance); - //System.out.println(ioGeom[1]); - return ioGeom; + + private void checkTPSNoChange(String wkt, double tolerance) { + checkTPS(wkt, tolerance, wkt); } } + diff --git a/modules/core/src/test/java/test/jts/GeometryTestCase.java b/modules/core/src/test/java/test/jts/GeometryTestCase.java index b6faa30fe0..de8201e17a 100644 --- a/modules/core/src/test/java/test/jts/GeometryTestCase.java +++ b/modules/core/src/test/java/test/jts/GeometryTestCase.java @@ -60,6 +60,10 @@ protected GeometryFactory getGeometryFactory() { return geomFactory; } + protected void checkValid(Geometry geom) { + assertTrue(geom.isValid()); + } + /** * Checks that the normalized values of the expected and actual * geometries are exactly equal. From b3d1ae8d6ff3486e0bf005e363c2572939181747 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 6 Nov 2023 15:30:06 -0800 Subject: [PATCH 021/123] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 93c47b0089..a8bfc2995c 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -36,6 +36,7 @@ Distributions for older JTS versions can be obtained at the * Improve `OffsetCurve` to return more linework for some input situations (#956) * Reduce buffer curve short fillet segments (#960) * Added ability to specify boundary for `LargestEmptyCircle` (#973) +* Improve `DouglaPeuckerSimplifier` and `TopologyPreservingSimplifier` to handle ring endpoints (#1013) ### Bug Fixes * Fix `PreparedGeometry` handling of EMPTY elements (#904) From 03f3ac73be8b9b7db4077e89c16d7b5932929357 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 7 Nov 2023 13:17:34 -0800 Subject: [PATCH 022/123] Improve simplifier tests --- .../jts/simplify/DouglasPeuckerSimplifierTest.java | 4 ++-- .../jts/simplify/TopologyPreservingSimplifierTest.java | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/core/src/test/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifierTest.java index df940a62df..bfd8f56b82 100644 --- a/modules/core/src/test/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifierTest.java @@ -53,8 +53,8 @@ public void testPolygonReductionWithSplit() { public void testPolygonReduction() { checkDP("POLYGON ((120 120, 121 121, 122 122, 220 120, 180 199, 160 200, 140 199, 120 120))", - 10.0, - "POLYGON ((120 120, 220 120, 180 199, 160 200, 140 199, 120 120)))"); + 10, + "POLYGON ((120 120, 220 120, 180 199, 160 200, 140 199, 120 120))"); } public void testPolygonWithTouchingHole() { diff --git a/modules/core/src/test/java/org/locationtech/jts/simplify/TopologyPreservingSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/simplify/TopologyPreservingSimplifierTest.java index 06cdceca71..4e36250216 100644 --- a/modules/core/src/test/java/org/locationtech/jts/simplify/TopologyPreservingSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/simplify/TopologyPreservingSimplifierTest.java @@ -158,6 +158,11 @@ public void testLinearRingRemoveEndpoint() throws Exception { ); } + public void testPolygonKeepFlatEndpointWithTouch() throws Exception { + checkTPSNoChange("POLYGON ((0 0, 5 2.05, 10 0, 10 10, 0 10, 0 0), (5 2.1, 6 2, 6 4, 4 4, 4 2, 5 2.1))", + 0.1 ); + } + public void testPolygonKeepEndpointWithCross() throws Exception { checkTPS( "POLYGON ((50 52, 60 50, 90 60, 90 10, 10 10, 10 90, 60 90, 50 55, 40 80, 20 60, 40 50, 50 52))", From 8e7688335ea5d32453aea44def83b979e00d32af Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 7 Nov 2023 15:26:23 -0800 Subject: [PATCH 023/123] Improve DPSimplifierTest --- .../jts/simplify/DouglasPeuckerSimplifierTest.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/modules/core/src/test/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifierTest.java index bfd8f56b82..08829c2dd5 100644 --- a/modules/core/src/test/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifierTest.java @@ -58,29 +58,30 @@ public void testPolygonReduction() { } public void testPolygonWithTouchingHole() { - checkDP("POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))", + checkDP("POLYGON ((10 10, 10 90, 90 90, 90 10, 10 10), (80 20, 20 20, 20 80, 50 90, 80 80, 80 20))", 10, - "POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))"); + "POLYGON ((10 10, 10 90, 90 90, 90 10, 10 10), (80 20, 20 20, 20 80, 80 80, 80 20))"); } - public void testFlattishPolygon() { + + public void testPolygonFlattish() { checkDP("POLYGON ((0 0, 50 0, 53 0, 55 0, 100 0, 70 1, 60 1, 50 1, 40 1, 0 0))", 10, "POLYGON EMPTY"); } - public void testTinySquare() { + public void testPolygonTinySquare() { checkDP("POLYGON ((0 5, 5 5, 5 0, 0 0, 0 1, 0 5))", 10, "POLYGON EMPTY"); } - public void testTinyHole() { + public void testPolygonTinyHole() { checkDP("POLYGON ((10 10, 10 310, 370 310, 370 10, 10 10), (160 190, 180 190, 180 170, 160 190))", 30, "POLYGON ((10 10, 10 310, 370 310, 370 10, 10 10))"); } - public void testTinyLineString() { + public void testLineStringTiny() { checkDP("LINESTRING (0 5, 1 5, 2 5, 5 5)", 10, "LINESTRING (0 5, 5 5)"); From c7e6c6f464a0a2c6c673261f1c2c9d5f3cd0f1b2 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 7 Nov 2023 15:27:05 -0800 Subject: [PATCH 024/123] Add TestSimplfiy XML test --- .../geomop/TestCaseGeometryFunctions.java | 10 + .../testxml/general/TestSimplify.xml | 207 ++++++++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 modules/tests/src/test/resources/testxml/general/TestSimplify.xml diff --git a/modules/tests/src/main/java/org/locationtech/jtstest/geomop/TestCaseGeometryFunctions.java b/modules/tests/src/main/java/org/locationtech/jtstest/geomop/TestCaseGeometryFunctions.java index 43778e5e96..bd2b468cba 100644 --- a/modules/tests/src/main/java/org/locationtech/jtstest/geomop/TestCaseGeometryFunctions.java +++ b/modules/tests/src/main/java/org/locationtech/jtstest/geomop/TestCaseGeometryFunctions.java @@ -23,6 +23,8 @@ import org.locationtech.jts.operation.polygonize.Polygonizer; import org.locationtech.jts.precision.GeometryPrecisionReducer; import org.locationtech.jts.precision.MinimumClearance; +import org.locationtech.jts.simplify.DouglasPeuckerSimplifier; +import org.locationtech.jts.simplify.TopologyPreservingSimplifier; /** * Geometry functions which @@ -79,6 +81,14 @@ public static Geometry polygonizeValidPolygonal(Geometry g) { return polygonize(g, true); } + public static Geometry simplifyDP(Geometry g, double distance) { + return DouglasPeuckerSimplifier.simplify(g, distance); + } + + public static Geometry simplifyTP(Geometry g, double distance) { + return TopologyPreservingSimplifier.simplify(g, distance); + } + public static Geometry reducePrecision(Geometry g, double scaleFactor) { return GeometryPrecisionReducer.reduce(g, new PrecisionModel(scaleFactor)); } diff --git a/modules/tests/src/test/resources/testxml/general/TestSimplify.xml b/modules/tests/src/test/resources/testxml/general/TestSimplify.xml new file mode 100644 index 0000000000..598d1413a6 --- /dev/null +++ b/modules/tests/src/test/resources/testxml/general/TestSimplify.xml @@ -0,0 +1,207 @@ + + Test cases for DouglasPeuckerSimplifier + + + + P - point + POINT(10 10) + + + POINT(10 10) + + + POINT(10 10) + + + + + mP - point with EMPTY + MULTIPOINT( EMPTY, (10 10), (20 20)) + + + MULTIPOINT((10 10), (20 20)) + + + MULTIPOINT((10 10), (20 20)) + + + + + L - empty line + LINESTRING EMPTY + + + LINESTRING EMPTY + + + LINESTRING EMPTY + + + + + L - line + LINESTRING (10 10, 20 21, 30 30) + + + LINESTRING (10 10, 30 30) + + + LINESTRING (10 10, 30 30) + + + + + L - short line + LINESTRING (0 5, 1 5, 2 5, 5 5) + + + LINESTRING (0 5, 5 5) + + + LINESTRING (0 5, 5 5) + + + + + mL - lines with EMPTY + MULTILINESTRING(EMPTY, (10 10, 20 21, 30 30), (10 10, 10 30, 30 30)) + + + MULTILINESTRING ((10 10, 30 30), (10 10, 10 30, 30 30)) + + + MULTILINESTRING ((10 10, 30 30), (10 10, 10 30, 30 30)) + + + + + A - polygon with flat endpoint + POLYGON ((5 1, 1 1, 1 9, 9 9, 9 1, 5 1)) + + + POLYGON ((1 1, 1 9, 9 9, 9 1, 1 1)) + + + POLYGON ((1 1, 1 9, 9 9, 9 1, 1 1)) + + + + + A - polygon simplification + POLYGON ((10 10, 10 90, 60.5 87, 90 90, 90 10, 12 12, 10 10)) + + + POLYGON ((10 10, 10 90, 90 90, 90 10, 10 10)) + + + POLYGON ((10 10, 10 90, 90 90, 90 10, 10 10)) + + + + + A - polygon with edge collapse. + DP: polygon is split + TP: unchanged + + POLYGON ((40 240, 160 241, 280 240, 280 160, 160 240, 40 140, 40 240)) + + + MULTIPOLYGON (((40 240, 160 240, 40 140, 40 240)), ((160 240, 280 240, 280 160, 160 240))) + + + POLYGON ((40 240, 160 241, 280 240, 280 160, 160 240, 40 140, 40 240)) + + + + + A - polygon collapse for DP + POLYGON ((5 2, 9 1, 1 1, 5 2)) + + + POLYGON EMPTY + + + POLYGON ((5 2, 9 1, 1 1, 5 2)) + + + + + A - polygon with touching hole + POLYGON ((10 10, 10 90, 90 90, 90 10, 10 10), (80 20, 20 20, 20 80, 50 90, 80 80, 80 20)) + + + POLYGON ((10 10, 10 90, 90 90, 90 10, 10 10), (80 20, 20 20, 20 80, 80 80, 80 20)) + + + POLYGON ((10 10, 10 90, 90 90, 90 10, 10 10), (80 20, 20 20, 20 80, 80 80, 80 20)) + + + + + A - polygon with large hole near edge. + DP: simplified and fixed + TP: unchanged + + POLYGON ((10 10, 10 80, 50 90, 90 80, 90 10, 10 10), (80 20, 20 20, 50 90, 80 20)) + + + POLYGON ((10 10, 10 80, 45.714285714285715 80, 20 20, 80 20, 54.285714285714285 80, 90 80, 90 10, 10 10)) + + + POLYGON ((10 10, 10 80, 50 90, 90 80, 90 10, 10 10), (80 20, 20 20, 50 90, 80 20)) + + + + + A - polygon with small hole near simplified edge + DP: hole is remmoved + TP: hole is skipped over - BUG! + + POLYGON ((10 10, 10 80, 50 90, 90 80, 90 10, 10 10), (70 81, 30 81, 50 90, 70 81)) + + + POLYGON ((10 10, 10 80, 90 80, 90 10, 10 10)) + + + POLYGON ((10 10, 10 80, 90 80, 90 10, 10 10), (70 81, 30 81, 50 90, 70 81)) + + + + + mA - multipolygon with EMPTY + MULTIPOLYGON (EMPTY, ((10 90, 10 10, 90 10, 50 60, 10 90)), ((70 90, 90 90, 90 70, 70 70, 70 90))) + + + MULTIPOLYGON (((10 90, 10 10, 90 10, 10 90)), ((70 90, 90 90, 90 70, 70 70, 70 90))) + + + MULTIPOLYGON (((10 90, 10 10, 90 10, 50 60, 10 90)), ((70 90, 90 90, 90 70, 70 70, 70 90))) + + + + + mA - multipolygon with small element removed + MULTIPOLYGON (((10 90, 10 10, 90 10, 50 60, 10 90)), ((90 90, 90 85, 85 85, 85 90, 90 90))) + + + POLYGON ((10 90, 10 10, 90 10, 10 90)) + + + MULTIPOLYGON (((10 90, 10 10, 90 10, 50 60, 10 90)), ((85 90, 90 85, 85 85, 85 90))) + + + + + GC - geometry collection + GEOMETRYCOLLECTION (POLYGON ((10 90, 10 10, 90 10, 50 60, 10 90)), LINESTRING (30 90, 65 65, 90 30), MULTIPOINT ((80 90), (90 90))) + + + GEOMETRYCOLLECTION (POLYGON ((10 90, 10 10, 90 10, 10 90)), LINESTRING (30 90, 90 30), MULTIPOINT ((80 90), (90 90))) + + + GEOMETRYCOLLECTION (POLYGON ((10 90, 10 10, 90 10, 50 60, 10 90)), LINESTRING (30 90, 90 30), MULTIPOINT ((80 90), (90 90))) + + + + + From a13b7a554b7f68aed85e85a4324060136dedf350 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 7 Nov 2023 15:49:51 -0800 Subject: [PATCH 025/123] Add TPSimplifier short polygon test --- .../jts/simplify/TopologyPreservingSimplifierTest.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/modules/core/src/test/java/org/locationtech/jts/simplify/TopologyPreservingSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/simplify/TopologyPreservingSimplifierTest.java index 4e36250216..e30cc26ce1 100644 --- a/modules/core/src/test/java/org/locationtech/jts/simplify/TopologyPreservingSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/simplify/TopologyPreservingSimplifierTest.java @@ -180,6 +180,15 @@ public void testPolygonRemoveFlatEndpoint() throws Exception { ); } + //-- vertex is not removed due to overly-restrictive heuristic result length calculation? + public void testPolygonSize5NotSimplfied() throws Exception { + checkTPS( + "POLYGON ((10 90, 10 10, 90 10, 47 57, 10 90))", + 10, + "POLYGON ((10 90, 10 10, 90 10, 47 57, 10 90))" + ); + } + /** * Test is from http://postgis.refractions.net/pipermail/postgis-users/2008-April/019327.html * Exhibits the issue where simplified polygon shells can "jump" across From f36b892153d74c4568a8d2244007367247ec8ef7 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 7 Nov 2023 15:50:17 -0800 Subject: [PATCH 026/123] Improve TestSimplify XML tests --- .../test/resources/testxml/general/TestSimplify.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/tests/src/test/resources/testxml/general/TestSimplify.xml b/modules/tests/src/test/resources/testxml/general/TestSimplify.xml index 598d1413a6..6873f0a574 100644 --- a/modules/tests/src/test/resources/testxml/general/TestSimplify.xml +++ b/modules/tests/src/test/resources/testxml/general/TestSimplify.xml @@ -181,25 +181,25 @@ mA - multipolygon with small element removed - MULTIPOLYGON (((10 90, 10 10, 90 10, 50 60, 10 90)), ((90 90, 90 85, 85 85, 85 90, 90 90))) + MULTIPOLYGON (((10 90, 10 10, 40 40, 90 10, 47 57, 10 90)), ((90 90, 90 85, 85 85, 85 90, 90 90))) - POLYGON ((10 90, 10 10, 90 10, 10 90)) + POLYGON ((10 90, 10 10, 40 40, 90 10, 10 90)) - MULTIPOLYGON (((10 90, 10 10, 90 10, 50 60, 10 90)), ((85 90, 90 85, 85 85, 85 90))) + MULTIPOLYGON (((10 90, 10 10, 40 40, 90 10, 10 90)), ((85 90, 90 85, 85 85, 85 90))) GC - geometry collection - GEOMETRYCOLLECTION (POLYGON ((10 90, 10 10, 90 10, 50 60, 10 90)), LINESTRING (30 90, 65 65, 90 30), MULTIPOINT ((80 90), (90 90))) + GEOMETRYCOLLECTION (POLYGON ((10 90, 10 10, 40 40, 90 10, 47 57, 10 90)), LINESTRING (30 90, 65 65, 90 30), MULTIPOINT ((80 90), (90 90))) - GEOMETRYCOLLECTION (POLYGON ((10 90, 10 10, 90 10, 10 90)), LINESTRING (30 90, 90 30), MULTIPOINT ((80 90), (90 90))) + GEOMETRYCOLLECTION (POLYGON ((10 90, 10 10, 40 40, 90 10, 10 90)), LINESTRING (30 90, 90 30), MULTIPOINT ((80 90), (90 90))) - GEOMETRYCOLLECTION (POLYGON ((10 90, 10 10, 90 10, 50 60, 10 90)), LINESTRING (30 90, 90 30), MULTIPOINT ((80 90), (90 90))) + GEOMETRYCOLLECTION (POLYGON ((10 90, 10 10, 40 40, 90 10, 10 90)), LINESTRING (30 90, 90 30), MULTIPOINT ((80 90), (90 90))) From 89309b2d688190416d2ef6a644d0dc37314f03b0 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 8 Nov 2023 08:41:22 -0800 Subject: [PATCH 027/123] Fix TestSimplify.xml description --- .../tests/src/test/resources/testxml/general/TestSimplify.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/tests/src/test/resources/testxml/general/TestSimplify.xml b/modules/tests/src/test/resources/testxml/general/TestSimplify.xml index 6873f0a574..72e611de03 100644 --- a/modules/tests/src/test/resources/testxml/general/TestSimplify.xml +++ b/modules/tests/src/test/resources/testxml/general/TestSimplify.xml @@ -1,5 +1,5 @@ - Test cases for DouglasPeuckerSimplifier + Test cases for DouglasPeuckerSimplifier and TopologyPreservingSimplifier From ad3a11485ac725f77f8cf21c4cf5f7cd911f4ef0 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 8 Nov 2023 09:27:43 -0800 Subject: [PATCH 028/123] Fix predicates for MultiPoint with EMPTY (#1015) --- .../jts/algorithm/PointLocator.java | 3 +++ .../jts/geomgraph/GeometryGraph.java | 4 ++- .../jts/operation/relate/RelateTest.java | 6 +++++ .../testxml/general/TestRelatePA.xml | 25 +++++++++++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/PointLocator.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/PointLocator.java index 3e2f469463..0a08fad14a 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/PointLocator.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/PointLocator.java @@ -107,6 +107,9 @@ else if (geom instanceof Polygon) { private void computeLocation(Coordinate p, Geometry geom) { + if (geom.isEmpty()) + return; + if (geom instanceof Point) { updateLocationInfo(locateOnPoint(p, (Point) geom)); } diff --git a/modules/core/src/main/java/org/locationtech/jts/geomgraph/GeometryGraph.java b/modules/core/src/main/java/org/locationtech/jts/geomgraph/GeometryGraph.java index 6e3f824ef9..4292399a88 100644 --- a/modules/core/src/main/java/org/locationtech/jts/geomgraph/GeometryGraph.java +++ b/modules/core/src/main/java/org/locationtech/jts/geomgraph/GeometryGraph.java @@ -222,7 +222,9 @@ private void addCollection(GeometryCollection gc) { for (int i = 0; i < gc.getNumGeometries(); i++) { Geometry g = gc.getGeometryN(i); - add(g); + if (! g.isEmpty()) { + add(g); + } } } /** diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relate/RelateTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relate/RelateTest.java index 8b7ee3e872..51975ea0be 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/relate/RelateTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relate/RelateTest.java @@ -101,6 +101,12 @@ public void testIntersectsSnappedEndpoint2() runRelateTest(a, b, "FF10F0102" ); } + public void testMultiPointWithEmpty() + { + String a = "MULTIPOINT(EMPTY,(0 0))"; + String b = "POLYGON ((1 0,0 1,-1 0,0 -1, 1 0))"; + runRelateTest(a, b, "0FFFFF212" ); + } void runRelateTest(String wkt1, String wkt2, String expectedIM) { diff --git a/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml b/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml index 7d9e4c5376..35a6f75bf9 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml @@ -100,4 +100,29 @@ + + mPA - empty Point element + + MULTIPOINT(EMPTY,(0 0)) + + + POLYGON ((1 0,0 1,-1 0,0 -1, 1 0)) + + + + true + + + false + true + false + false + false + false + true + false + false + true + + From 98ede4114dd70dd49f47b1171c26ebbcb76650c3 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 8 Nov 2023 09:28:19 -0800 Subject: [PATCH 029/123] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index a8bfc2995c..6b45106661 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -63,6 +63,7 @@ Distributions for older JTS versions can be obtained at the * Fix OverlayNG Area Check heuristic for difference (#1005) * Fix `InteriorPointPoint` to handle empty elements * Fix `DistanceOp` for empty elements (#1010) +* Fix predicates for MultiPoint with EMPTY (#1015) ### Performance Improvements From ea9f2bf69bd0559dc64a49b547fb558e568f52d4 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 8 Nov 2023 09:34:05 -0800 Subject: [PATCH 030/123] XML predicate test formatting --- .../testxml/general/TestRelateAA.xml | 2 +- .../testxml/validate/TestRelateAA.xml | 2140 ++++++------ .../testxml/validate/TestRelateAC.xml | 20 +- .../testxml/validate/TestRelateLA.xml | 1540 ++++----- .../testxml/validate/TestRelateLC.xml | 40 +- .../testxml/validate/TestRelateLL.xml | 2880 ++++++++--------- .../testxml/validate/TestRelatePA.xml | 820 ++--- .../testxml/validate/TestRelatePL.xml | 1980 ++++++------ .../testxml/validate/TestRelatePP.xml | 260 +- 9 files changed, 4841 insertions(+), 4841 deletions(-) diff --git a/modules/tests/src/test/resources/testxml/general/TestRelateAA.xml b/modules/tests/src/test/resources/testxml/general/TestRelateAA.xml index d8694b908e..eca8a98ca2 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRelateAA.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRelateAA.xml @@ -99,7 +99,7 @@ (120 140, 260 140, 260 260, 120 260, 120 140)) - true + true true false diff --git a/modules/tests/src/test/resources/testxml/validate/TestRelateAA.xml b/modules/tests/src/test/resources/testxml/validate/TestRelateAA.xml index bb9c3eae1d..a2e530a433 100644 --- a/modules/tests/src/test/resources/testxml/validate/TestRelateAA.xml +++ b/modules/tests/src/test/resources/testxml/validate/TestRelateAA.xml @@ -14,16 +14,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -39,16 +39,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -64,16 +64,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -89,16 +89,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -114,16 +114,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -139,16 +139,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -164,16 +164,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -189,16 +189,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -214,16 +214,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -239,16 +239,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -264,16 +264,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -289,16 +289,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -314,16 +314,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -339,16 +339,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -365,16 +365,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -391,16 +391,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -416,16 +416,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -441,16 +441,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -466,16 +466,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -491,16 +491,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -516,16 +516,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -541,16 +541,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -566,16 +566,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -591,16 +591,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -616,16 +616,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -641,16 +641,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -666,16 +666,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -691,16 +691,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -716,16 +716,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -741,16 +741,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -766,16 +766,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -791,16 +791,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -816,16 +816,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -841,16 +841,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -866,16 +866,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -891,16 +891,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -916,16 +916,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -941,16 +941,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -966,16 +966,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -991,16 +991,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1016,16 +1016,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1041,16 +1041,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1066,16 +1066,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1091,16 +1091,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1116,16 +1116,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1141,16 +1141,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1166,16 +1166,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1191,16 +1191,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1216,16 +1216,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1241,16 +1241,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1266,16 +1266,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1291,16 +1291,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1316,16 +1316,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1362,16 +1362,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1414,16 +1414,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1439,16 +1439,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1464,16 +1464,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1489,16 +1489,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1514,16 +1514,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1539,16 +1539,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1564,16 +1564,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1589,16 +1589,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1614,16 +1614,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1639,16 +1639,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1664,16 +1664,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1689,16 +1689,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1714,16 +1714,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1739,16 +1739,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1764,16 +1764,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1789,16 +1789,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1814,16 +1814,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1839,16 +1839,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1864,16 +1864,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1889,16 +1889,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1914,16 +1914,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1939,16 +1939,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1965,16 +1965,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1991,16 +1991,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2017,16 +2017,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2043,16 +2043,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2069,16 +2069,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2095,16 +2095,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2121,16 +2121,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2147,16 +2147,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2173,16 +2173,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2199,16 +2199,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2225,16 +2225,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2251,16 +2251,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2278,16 +2278,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2304,16 +2304,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2330,16 +2330,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2356,16 +2356,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2383,16 +2383,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -2412,16 +2412,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -2440,16 +2440,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2468,16 +2468,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2496,16 +2496,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2524,16 +2524,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2552,16 +2552,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2580,16 +2580,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2612,16 +2612,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -2643,16 +2643,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2678,16 +2678,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2713,16 +2713,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2748,16 +2748,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2783,16 +2783,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2818,16 +2818,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false diff --git a/modules/tests/src/test/resources/testxml/validate/TestRelateAC.xml b/modules/tests/src/test/resources/testxml/validate/TestRelateAC.xml index 526288b468..1e4feca114 100644 --- a/modules/tests/src/test/resources/testxml/validate/TestRelateAC.xml +++ b/modules/tests/src/test/resources/testxml/validate/TestRelateAC.xml @@ -21,16 +21,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false diff --git a/modules/tests/src/test/resources/testxml/validate/TestRelateLA.xml b/modules/tests/src/test/resources/testxml/validate/TestRelateLA.xml index 3be916eae9..4f19f28800 100644 --- a/modules/tests/src/test/resources/testxml/validate/TestRelateLA.xml +++ b/modules/tests/src/test/resources/testxml/validate/TestRelateLA.xml @@ -13,16 +13,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -37,16 +37,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -61,16 +61,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -85,16 +85,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -109,16 +109,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -133,16 +133,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -157,16 +157,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -181,16 +181,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -205,16 +205,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -229,16 +229,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -253,16 +253,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -277,16 +277,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -301,16 +301,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -325,16 +325,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -349,16 +349,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -373,16 +373,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -397,16 +397,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -421,16 +421,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -445,16 +445,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -470,16 +470,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -495,16 +495,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -520,16 +520,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -545,16 +545,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -570,16 +570,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -595,16 +595,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -620,16 +620,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -645,16 +645,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -670,16 +670,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -695,16 +695,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -720,16 +720,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -745,16 +745,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -771,16 +771,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -797,16 +797,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -823,16 +823,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -849,16 +849,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -875,16 +875,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -901,16 +901,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -927,16 +927,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -953,16 +953,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -980,16 +980,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1007,16 +1007,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1034,16 +1034,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1058,16 +1058,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1082,16 +1082,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1107,16 +1107,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1131,16 +1131,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1156,16 +1156,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1181,16 +1181,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1206,16 +1206,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1230,16 +1230,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1255,16 +1255,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1279,16 +1279,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1304,16 +1304,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1329,16 +1329,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1354,16 +1354,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1379,16 +1379,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1403,16 +1403,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1427,16 +1427,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1451,16 +1451,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1475,16 +1475,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1499,16 +1499,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1523,16 +1523,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1547,16 +1547,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1571,16 +1571,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1596,16 +1596,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1622,16 +1622,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1648,16 +1648,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1674,16 +1674,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1700,16 +1700,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1726,16 +1726,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1752,16 +1752,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1778,16 +1778,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1804,16 +1804,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1830,16 +1830,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1859,16 +1859,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1888,16 +1888,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1917,16 +1917,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false diff --git a/modules/tests/src/test/resources/testxml/validate/TestRelateLC.xml b/modules/tests/src/test/resources/testxml/validate/TestRelateLC.xml index c002318b33..0bb1205fb6 100644 --- a/modules/tests/src/test/resources/testxml/validate/TestRelateLC.xml +++ b/modules/tests/src/test/resources/testxml/validate/TestRelateLC.xml @@ -16,16 +16,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -42,16 +42,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true diff --git a/modules/tests/src/test/resources/testxml/validate/TestRelateLL.xml b/modules/tests/src/test/resources/testxml/validate/TestRelateLL.xml index 0308d75e63..8124b25ab6 100644 --- a/modules/tests/src/test/resources/testxml/validate/TestRelateLL.xml +++ b/modules/tests/src/test/resources/testxml/validate/TestRelateLL.xml @@ -12,16 +12,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -35,16 +35,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -58,16 +58,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -81,16 +81,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -104,16 +104,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -127,16 +127,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -150,16 +150,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -173,16 +173,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -196,16 +196,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -219,16 +219,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -242,16 +242,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -265,16 +265,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -288,16 +288,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -311,16 +311,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -334,16 +334,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -357,16 +357,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -380,16 +380,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -403,16 +403,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -426,16 +426,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -449,16 +449,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -472,16 +472,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -495,16 +495,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -518,16 +518,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -541,16 +541,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -564,16 +564,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -587,16 +587,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -610,16 +610,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -633,16 +633,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -656,16 +656,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -679,16 +679,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -702,16 +702,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -725,16 +725,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -748,16 +748,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -771,16 +771,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -794,16 +794,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -817,16 +817,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -840,16 +840,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -863,16 +863,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -886,16 +886,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -909,16 +909,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -932,16 +932,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -955,16 +955,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -978,16 +978,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1001,16 +1001,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1024,16 +1024,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1047,16 +1047,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1071,16 +1071,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1102,16 +1102,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1125,16 +1125,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -1148,16 +1148,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -1171,16 +1171,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -1194,16 +1194,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -1217,16 +1217,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -1240,16 +1240,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -1263,16 +1263,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -1286,16 +1286,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1309,16 +1309,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1332,16 +1332,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1355,16 +1355,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1378,16 +1378,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1401,16 +1401,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1424,16 +1424,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1447,16 +1447,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1470,16 +1470,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1493,16 +1493,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1516,16 +1516,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1539,16 +1539,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1562,16 +1562,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1585,16 +1585,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1609,16 +1609,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1632,16 +1632,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1655,16 +1655,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1678,16 +1678,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1701,16 +1701,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1724,16 +1724,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1747,16 +1747,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1770,16 +1770,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1793,16 +1793,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1816,16 +1816,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1839,16 +1839,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1862,16 +1862,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1885,16 +1885,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1908,16 +1908,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1931,16 +1931,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1954,16 +1954,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1977,16 +1977,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2000,16 +2000,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2023,16 +2023,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2046,16 +2046,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2069,16 +2069,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2092,16 +2092,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2115,16 +2115,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2138,16 +2138,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2161,16 +2161,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2184,16 +2184,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2207,16 +2207,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2230,16 +2230,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2253,16 +2253,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2276,16 +2276,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2299,16 +2299,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2322,16 +2322,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2345,16 +2345,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2368,16 +2368,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2391,16 +2391,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -2414,16 +2414,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -2439,16 +2439,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2462,16 +2462,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -2485,16 +2485,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -2508,16 +2508,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -2531,16 +2531,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -2554,16 +2554,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2577,16 +2577,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2600,16 +2600,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2623,16 +2623,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2646,16 +2646,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2669,16 +2669,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2692,16 +2692,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -2715,16 +2715,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -2738,16 +2738,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -2761,16 +2761,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2784,16 +2784,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2807,16 +2807,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2830,16 +2830,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -2853,16 +2853,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -2876,16 +2876,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2899,16 +2899,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2922,16 +2922,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2945,16 +2945,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2968,16 +2968,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2991,16 +2991,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -3018,16 +3018,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -3045,16 +3045,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -3072,16 +3072,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -3100,16 +3100,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -3127,16 +3127,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -3154,16 +3154,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -3182,16 +3182,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -3210,16 +3210,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -3238,16 +3238,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -3265,16 +3265,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -3292,16 +3292,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -3319,16 +3319,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -3346,16 +3346,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -3373,16 +3373,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false diff --git a/modules/tests/src/test/resources/testxml/validate/TestRelatePA.xml b/modules/tests/src/test/resources/testxml/validate/TestRelatePA.xml index 643e76603a..7d864c36bd 100644 --- a/modules/tests/src/test/resources/testxml/validate/TestRelatePA.xml +++ b/modules/tests/src/test/resources/testxml/validate/TestRelatePA.xml @@ -13,16 +13,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -37,16 +37,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -61,16 +61,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -85,16 +85,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -109,16 +109,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -133,16 +133,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -157,16 +157,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -181,16 +181,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -205,16 +205,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -230,16 +230,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -255,16 +255,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -280,16 +280,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -305,16 +305,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -330,16 +330,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -355,16 +355,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -380,16 +380,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -405,16 +405,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -430,16 +430,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -456,16 +456,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -482,16 +482,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -506,16 +506,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -530,16 +530,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -554,16 +554,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -578,16 +578,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -602,16 +602,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -626,16 +626,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -650,16 +650,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -674,16 +674,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -698,16 +698,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -722,16 +722,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -746,16 +746,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -770,16 +770,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -794,16 +794,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -818,16 +818,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -843,16 +843,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -868,16 +868,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -893,16 +893,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -920,16 +920,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -947,16 +947,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -974,16 +974,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1003,16 +1003,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false diff --git a/modules/tests/src/test/resources/testxml/validate/TestRelatePL.xml b/modules/tests/src/test/resources/testxml/validate/TestRelatePL.xml index 2e11fb7b9e..30972a07e2 100644 --- a/modules/tests/src/test/resources/testxml/validate/TestRelatePL.xml +++ b/modules/tests/src/test/resources/testxml/validate/TestRelatePL.xml @@ -12,16 +12,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -35,16 +35,16 @@ true - true - true - true - false - false - false - true - false - false - true + true + true + true + false + false + false + true + false + false + true @@ -58,16 +58,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -81,16 +81,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -104,16 +104,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -127,16 +127,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -150,16 +150,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -173,16 +173,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -196,16 +196,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -219,16 +219,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -242,16 +242,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -265,16 +265,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -288,16 +288,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -311,16 +311,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -334,16 +334,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -357,16 +357,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -380,16 +380,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -403,16 +403,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -426,16 +426,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -449,16 +449,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -472,16 +472,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -495,16 +495,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -518,16 +518,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -541,16 +541,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -564,16 +564,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -587,16 +587,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -610,16 +610,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -633,16 +633,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -657,16 +657,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -680,16 +680,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -703,16 +703,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -726,16 +726,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -749,16 +749,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -772,16 +772,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -795,16 +795,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -818,16 +818,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -841,16 +841,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -864,16 +864,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -887,16 +887,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -910,16 +910,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -933,16 +933,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -956,16 +956,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -979,16 +979,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1002,16 +1002,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1025,16 +1025,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1048,16 +1048,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1071,16 +1071,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1094,16 +1094,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1117,16 +1117,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1140,16 +1140,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1163,16 +1163,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1186,16 +1186,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1209,16 +1209,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1232,16 +1232,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1255,16 +1255,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1278,16 +1278,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1301,16 +1301,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1324,16 +1324,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1347,16 +1347,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1370,16 +1370,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1394,16 +1394,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1417,16 +1417,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1441,16 +1441,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1464,16 +1464,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1487,16 +1487,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1510,16 +1510,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1533,16 +1533,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1557,16 +1557,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1580,16 +1580,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1604,16 +1604,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1627,16 +1627,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1650,16 +1650,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1673,16 +1673,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1696,16 +1696,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1719,16 +1719,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1742,16 +1742,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1765,16 +1765,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1788,16 +1788,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1811,16 +1811,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1834,16 +1834,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1857,16 +1857,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1880,16 +1880,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1903,16 +1903,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1926,16 +1926,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -1949,16 +1949,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -1972,16 +1972,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1995,16 +1995,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2018,16 +2018,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2041,16 +2041,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2064,16 +2064,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2087,16 +2087,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2110,16 +2110,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2133,16 +2133,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2156,16 +2156,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2179,16 +2179,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2202,16 +2202,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2225,16 +2225,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2248,16 +2248,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2271,16 +2271,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true diff --git a/modules/tests/src/test/resources/testxml/validate/TestRelatePP.xml b/modules/tests/src/test/resources/testxml/validate/TestRelatePP.xml index 847908e5ba..e4d062902e 100644 --- a/modules/tests/src/test/resources/testxml/validate/TestRelatePP.xml +++ b/modules/tests/src/test/resources/testxml/validate/TestRelatePP.xml @@ -12,16 +12,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -35,16 +35,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -58,16 +58,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -81,16 +81,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -104,16 +104,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -127,16 +127,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -150,16 +150,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -173,16 +173,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -196,16 +196,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -219,16 +219,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -242,16 +242,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -265,16 +265,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -288,16 +288,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false From 7afe475606f7958a7d1c8e63fd5cee0c50e53354 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 8 Nov 2023 17:22:45 -0800 Subject: [PATCH 031/123] Add TestRelatePA.xml test --- .../testxml/general/TestRelatePA.xml | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml b/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml index 35a6f75bf9..648992d4ae 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml @@ -101,7 +101,7 @@ - mPA - empty Point element + mPA - empty MultiPoint element MULTIPOINT(EMPTY,(0 0)) @@ -125,4 +125,29 @@ true + + PmA - empty MultiPolygon element + + POINT(0 0) + + + MULTIPOLYGON (EMPTY, ((1 0,0 1,-1 0,0 -1, 1 0))) + + + + true + + + false + true + false + false + false + false + true + false + false + true + + From f95b42ee066c8e8c53eb92e036e95ee578a846cd Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 8 Nov 2023 19:30:55 -0800 Subject: [PATCH 032/123] Consolidate some XML unit tests --- .../testxml/general/TestRelateAA.xml | 14 +++++++ .../testxml/general/TestRelateAC.xml | 27 ------------ .../testxml/general/TestRelateLC.xml | 41 ------------------- .../testxml/general/TestRelateLL.xml | 26 ++++++++++++ 4 files changed, 40 insertions(+), 68 deletions(-) delete mode 100644 modules/tests/src/test/resources/testxml/general/TestRelateAC.xml delete mode 100644 modules/tests/src/test/resources/testxml/general/TestRelateLC.xml diff --git a/modules/tests/src/test/resources/testxml/general/TestRelateAA.xml b/modules/tests/src/test/resources/testxml/general/TestRelateAA.xml index eca8a98ca2..bbee510c89 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRelateAA.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRelateAA.xml @@ -232,4 +232,18 @@ false + + A/mA A-shells overlapping B-shell at A-vertex + + POLYGON ((100 60, 140 100, 100 140, 60 100, 100 60)) + + + MULTIPOLYGON (((80 40, 120 40, 120 80, 80 80, 80 40)), ((120 80, 160 80, 160 120, 120 120, 120 80)), ((80 120, 120 120, 120 160, 80 160, 80 120)), ((40 80, 80 80, 80 120, 40 120, 40 80))) + + + true + + true + + diff --git a/modules/tests/src/test/resources/testxml/general/TestRelateAC.xml b/modules/tests/src/test/resources/testxml/general/TestRelateAC.xml deleted file mode 100644 index 99d593f9c0..0000000000 --- a/modules/tests/src/test/resources/testxml/general/TestRelateAC.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - AC A-shells overlapping B-shell at A-vertex - - POLYGON( - (100 60, 140 100, 100 140, 60 100, 100 60)) - - - MULTIPOLYGON( - ( - (80 40, 120 40, 120 80, 80 80, 80 40)), - ( - (120 80, 160 80, 160 120, 120 120, 120 80)), - ( - (80 120, 120 120, 120 160, 80 160, 80 120)), - ( - (40 80, 80 80, 80 120, 40 120, 40 80))) - - - true - - true - - - diff --git a/modules/tests/src/test/resources/testxml/general/TestRelateLC.xml b/modules/tests/src/test/resources/testxml/general/TestRelateLC.xml deleted file mode 100644 index 864ce95589..0000000000 --- a/modules/tests/src/test/resources/testxml/general/TestRelateLC.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - LC - topographically equal with no boundary - - LINESTRING(0 0, 0 50, 50 50, 50 0, 0 0) - - - MULTILINESTRING( - (0 0, 0 50), - (0 50, 50 50), - (50 50, 50 0), - (50 0, 0 0)) - - - - true - - - - - - LC - equal with boundary intersection - - LINESTRING(0 0, 60 0, 60 60, 60 0, 120 0) - - - MULTILINESTRING( - (0 0, 60 0), - (60 0, 120 0), - (60 0, 60 60)) - - - - true - - - - - diff --git a/modules/tests/src/test/resources/testxml/general/TestRelateLL.xml b/modules/tests/src/test/resources/testxml/general/TestRelateLL.xml index 4a4fb4289d..592443d1f9 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRelateLL.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRelateLL.xml @@ -365,4 +365,30 @@ false + + LmL - topographically equal with no boundary + + LINESTRING(0 0, 0 50, 50 50, 50 0, 0 0) + + + MULTILINESTRING((0 0, 0 50), (0 50, 50 50), (50 50, 50 0), (50 0, 0 0)) + + + true + + + + + LmL - equal with boundary intersection + + LINESTRING(0 0, 60 0, 60 60, 60 0, 120 0) + + + MULTILINESTRING((0 0, 60 0), (60 0, 120 0), (60 0, 60 60)) + + + true + + + From 1915fdc01d6e95089e63936b8d3796327e67d2d8 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 9 Nov 2023 13:23:49 -0800 Subject: [PATCH 033/123] Add TestRelatePA tests --- .../testxml/general/TestRelatePA.xml | 69 +++++++++++++++++-- 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml b/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml index 648992d4ae..642d682b72 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml @@ -15,6 +15,16 @@ true + false + false + false + false + true + false + false + false + false + false @@ -31,6 +41,16 @@ true + false + false + false + true + false + false + true + false + false + false @@ -39,14 +59,23 @@ MULTIPOINT((0 20), (20 20)) - POLYGON( - (20 40, 20 0, 60 0, 60 40, 20 40)) + POLYGON((20 40, 20 0, 60 0, 60 40, 20 40)) true + false + false + false + false + false + false + true + false + true + false @@ -55,14 +84,23 @@ MULTIPOINT((20 20), (40 20)) - POLYGON( - (20 40, 20 0, 60 0, 60 40, 20 40)) + POLYGON((20 40, 20 0, 60 0, 60 40, 20 40)) true + false + true + false + false + false + false + true + false + false + true @@ -71,14 +109,23 @@ MULTIPOINT((80 260), (140 260), (180 260)) - POLYGON( - (40 320, 140 320, 140 200, 40 200, 40 320)) + POLYGON((40 320, 140 320, 140 200, 40 200, 40 320)) true + false + false + false + true + false + false + true + false + false + false @@ -98,6 +145,16 @@ true + false + true + false + false + false + false + true + false + true + false From 84bcead5d968ce8ef93b704d20a48620e212fd9a Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 9 Nov 2023 13:39:53 -0800 Subject: [PATCH 034/123] Improve XMLTestRunner test failure msg --- .../org/locationtech/jtstest/testrunner/SimpleReportWriter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/tests/src/main/java/org/locationtech/jtstest/testrunner/SimpleReportWriter.java b/modules/tests/src/main/java/org/locationtech/jtstest/testrunner/SimpleReportWriter.java index beed109b6a..04f14d048d 100644 --- a/modules/tests/src/main/java/org/locationtech/jtstest/testrunner/SimpleReportWriter.java +++ b/modules/tests/src/main/java/org/locationtech/jtstest/testrunner/SimpleReportWriter.java @@ -70,7 +70,7 @@ public void reportOnTest(Test test) id += " " + test.getArgument(i); } if (test.getExpectedResult() instanceof BooleanResult) { - id += ", " + test.getExpectedResult().toShortString(); + id += " -> " + test.getExpectedResult().toShortString(); } if (test.getDescription().length() > 0) { id += ", " + test.getDescription(); From 58d7bd54dd996f1deca433ffd0a67f8f5e5b928e Mon Sep 17 00:00:00 2001 From: Mike Taves Date: Thu, 16 Nov 2023 12:55:05 +1300 Subject: [PATCH 035/123] Add Angle.sinSnap and .cosSnap to avoid small errors, e.g. with buffer operations (#1016) --- .../org/locationtech/jts/algorithm/Angle.java | 32 ++++++++++++++--- .../operation/buffer/BufferParameters.java | 4 ++- .../buffer/OffsetSegmentGenerator.java | 24 ++++++------- .../jts/util/GeometricShapeFactory.java | 21 +++++------ .../locationtech/jts/algorithm/AngleTest.java | 36 ++++++++++++++++++- .../jts/operation/buffer/BufferTest.java | 19 ++++++++++ 6 files changed, 108 insertions(+), 28 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/Angle.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/Angle.java index 1da839b395..58dd05b34e 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/Angle.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/Angle.java @@ -313,12 +313,36 @@ public static double diff(double ang1, double ang2) { } if (delAngle > Math.PI) { - delAngle = (2 * Math.PI) - delAngle; + delAngle = PI_TIMES_2 - delAngle; } return delAngle; } - + + /** + * Computes sin of an angle, snapping near-zero values to zero. + * + * @param ang the input angle (in radians) + * @return the result of the trigonometric function + */ + public static double sinSnap(double ang) { + double res = Math.sin(ang); + if (Math.abs(res) < 5e-16) return 0.0; + return res; + } + + /** + * Computes cos of an angle, snapping near-zero values to zero. + * + * @param ang the input angle (in radians) + * @return the result of the trigonometric function + */ + public static double cosSnap(double ang) { + double res = Math.cos(ang); + if (Math.abs(res) < 5e-16) return 0.0; + return res; + } + /** * Projects a point by a given angle and distance. * @@ -328,8 +352,8 @@ public static double diff(double ang1, double ang2) { * @return the projected point */ public static Coordinate project(Coordinate p, double angle, double dist) { - double x = p.getX() + dist * Math.cos(angle); - double y = p.getY() + dist * Math.sin(angle); + double x = p.getX() + dist * Angle.cosSnap(angle); + double y = p.getY() + dist * Angle.sinSnap(angle); return new Coordinate(x, y); } } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferParameters.java b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferParameters.java index e9cc6f8799..f4799fc26b 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferParameters.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferParameters.java @@ -11,6 +11,8 @@ */ package org.locationtech.jts.operation.buffer; +import org.locationtech.jts.algorithm.Angle; + /** * A value class containing the parameters which * specify how a buffer should be constructed. @@ -176,7 +178,7 @@ public void setQuadrantSegments(int quadSegs) */ public static double bufferDistanceError(int quadSegs) { - double alpha = Math.PI / 2.0 / quadSegs; + double alpha = Angle.PI_OVER_2 / quadSegs; return 1 - Math.cos(alpha / 2.0); } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetSegmentGenerator.java b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetSegmentGenerator.java index f1591e4451..611d5e0c9b 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetSegmentGenerator.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetSegmentGenerator.java @@ -114,7 +114,7 @@ public OffsetSegmentGenerator(PrecisionModel precisionModel, int quadSegs = bufParams.getQuadrantSegments(); if (quadSegs < 1) quadSegs = 1; - filletAngleQuantum = Math.PI / 2.0 / quadSegs; + filletAngleQuantum = Angle.PI_OVER_2 / quadSegs; /** * Non-round joins cause issues with short closing segments, so don't use @@ -428,7 +428,7 @@ public void addLineEndCap(Coordinate p0, Coordinate p1) case BufferParameters.CAP_ROUND: // add offset seg points with a fillet between them segList.addPt(offsetL.p1); - addDirectedFillet(p1, angle + Math.PI / 2, angle - Math.PI / 2, Orientation.CLOCKWISE, distance); + addDirectedFillet(p1, angle + Angle.PI_OVER_2, angle - Angle.PI_OVER_2, Orientation.CLOCKWISE, distance); segList.addPt(offsetR.p1); break; case BufferParameters.CAP_FLAT: @@ -439,8 +439,8 @@ public void addLineEndCap(Coordinate p0, Coordinate p1) case BufferParameters.CAP_SQUARE: // add a square defined by extensions of the offset segment endpoints Coordinate squareCapSideOffset = new Coordinate(); - squareCapSideOffset.x = Math.abs(distance) * Math.cos(angle); - squareCapSideOffset.y = Math.abs(distance) * Math.sin(angle); + squareCapSideOffset.x = Math.abs(distance) * Angle.cosSnap(angle); + squareCapSideOffset.y = Math.abs(distance) * Angle.sinSnap(angle); Coordinate squareCapLOffset = new Coordinate( offsetL.p1.x + squareCapSideOffset.x, @@ -534,7 +534,7 @@ private void addLimitedMitreJoin( Coordinate bevelMidPt = project(cornerPt, -mitreLimitDistance, dirBisector); // direction of bevel segment (at right angle to corner bisector) - double dirBevel = Angle.normalize(dirBisector + Math.PI/2.0); + double dirBevel = Angle.normalize(dirBisector + Angle.PI_OVER_2); // compute the candidate bevel segment by projecting both sides of the midpoint Coordinate bevel0 = project(bevelMidPt, distance, dirBevel); @@ -567,8 +567,8 @@ private void addLimitedMitreJoin( * @return the projected point */ private static Coordinate project(Coordinate pt, double d, double dir) { - double x = pt.x + d * Math.cos(dir); - double y = pt.y + d * Math.sin(dir); + double x = pt.x + d * Angle.cosSnap(dir); + double y = pt.y + d * Angle.sinSnap(dir); return new Coordinate(x, y); } @@ -607,10 +607,10 @@ private void addCornerFillet(Coordinate p, Coordinate p0, Coordinate p1, int dir double endAngle = Math.atan2(dy1, dx1); if (direction == Orientation.CLOCKWISE) { - if (startAngle <= endAngle) startAngle += 2.0 * Math.PI; + if (startAngle <= endAngle) startAngle += Angle.PI_TIMES_2; } else { // direction == COUNTERCLOCKWISE - if (startAngle >= endAngle) startAngle -= 2.0 * Math.PI; + if (startAngle >= endAngle) startAngle -= Angle.PI_TIMES_2; } segList.addPt(p0); addDirectedFillet(p, startAngle, endAngle, direction, radius); @@ -641,8 +641,8 @@ private void addDirectedFillet(Coordinate p, double startAngle, double endAngle, Coordinate pt = new Coordinate(); for (int i = 0; i < nSegs; i++) { double angle = startAngle + directionFactor * i * angleInc; - pt.x = p.x + radius * Math.cos(angle); - pt.y = p.y + radius * Math.sin(angle); + pt.x = p.x + radius * Angle.cosSnap(angle); + pt.y = p.y + radius * Angle.sinSnap(angle); segList.addPt(pt); } } @@ -655,7 +655,7 @@ public void createCircle(Coordinate p) // add start point Coordinate pt = new Coordinate(p.x + distance, p.y); segList.addPt(pt); - addDirectedFillet(p, 0.0, 2.0 * Math.PI, -1, distance); + addDirectedFillet(p, 0.0, Angle.PI_TIMES_2, -1, distance); segList.closeRing(); } diff --git a/modules/core/src/main/java/org/locationtech/jts/util/GeometricShapeFactory.java b/modules/core/src/main/java/org/locationtech/jts/util/GeometricShapeFactory.java index 7ba7dd2ec9..90f5bb26a8 100644 --- a/modules/core/src/main/java/org/locationtech/jts/util/GeometricShapeFactory.java +++ b/modules/core/src/main/java/org/locationtech/jts/util/GeometricShapeFactory.java @@ -11,6 +11,7 @@ */ package org.locationtech.jts.util; +import org.locationtech.jts.algorithm.Angle; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; @@ -222,8 +223,8 @@ public Polygon createEllipse() int iPt = 0; for (int i = 0; i < nPts; i++) { double ang = i * (2 * Math.PI / nPts); - double x = xRadius * Math.cos(ang) + centreX; - double y = yRadius * Math.sin(ang) + centreY; + double x = xRadius * Angle.cosSnap(ang) + centreX; + double y = yRadius * Angle.sinSnap(ang) + centreY; pts[iPt++] = coord(x, y); } pts[iPt] = new Coordinate(pts[0]); @@ -319,16 +320,16 @@ public LineString createArc( double centreY = env.getMinY() + yRadius; double angSize = angExtent; - if (angSize <= 0.0 || angSize > 2 * Math.PI) - angSize = 2 * Math.PI; + if (angSize <= 0.0 || angSize > Angle.PI_TIMES_2) + angSize = Angle.PI_TIMES_2; double angInc = angSize / (nPts - 1); Coordinate[] pts = new Coordinate[nPts]; int iPt = 0; for (int i = 0; i < nPts; i++) { double ang = startAng + i * angInc; - double x = xRadius * Math.cos(ang) + centreX; - double y = yRadius * Math.sin(ang) + centreY; + double x = xRadius * Angle.cosSnap(ang) + centreX; + double y = yRadius * Angle.sinSnap(ang) + centreY; pts[iPt++] = coord(x, y); } LineString line = geomFact.createLineString(pts); @@ -353,8 +354,8 @@ public Polygon createArcPolygon(double startAng, double angExtent) { double centreY = env.getMinY() + yRadius; double angSize = angExtent; - if (angSize <= 0.0 || angSize > 2 * Math.PI) - angSize = 2 * Math.PI; + if (angSize <= 0.0 || angSize > Angle.PI_TIMES_2) + angSize = Angle.PI_TIMES_2; double angInc = angSize / (nPts - 1); // double check = angInc * nPts; // double checkEndAng = startAng + check; @@ -366,8 +367,8 @@ public Polygon createArcPolygon(double startAng, double angExtent) { for (int i = 0; i < nPts; i++) { double ang = startAng + angInc * i; - double x = xRadius * Math.cos(ang) + centreX; - double y = yRadius * Math.sin(ang) + centreY; + double x = xRadius * Angle.cosSnap(ang) + centreX; + double y = yRadius * Angle.sinSnap(ang) + centreY; pts[iPt++] = coord(x, y); } pts[iPt++] = coord(centreX, centreY); diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/AngleTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/AngleTest.java index 417bb1f538..9f005cef06 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/AngleTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/AngleTest.java @@ -158,7 +158,41 @@ public void testAngleBisector() { assertEquals(45, Math.toDegrees(Angle.bisector(p(13,10), p(10,10), p(10,20))), 0.01); } - + + public void testSinCosSnap() { + + // -720 to 720 degrees with 1 degree increments + for (int angdeg = -720; angdeg <= 720; angdeg++) { + double ang = Angle.toRadians(angdeg); + + double rSin = Angle.sinSnap(ang); + double rCos = Angle.cosSnap(ang); + + double cSin = Math.sin(ang); + double cCos = Math.cos(ang); + if ( (angdeg % 90) == 0 ) { + // not always the same for multiples of 90 degrees + assertTrue(Math.abs(rSin - cSin) < 1e-15); + assertTrue(Math.abs(rCos - cCos) < 1e-15); + } else { + assertEquals(rSin, cSin); + assertEquals(rCos, cCos); + } + + } + + // use radian increments that don't snap to exact degrees or zero + for (double angrad = -6.3; angrad < 6.3; angrad += 0.013) { + + double rSin = Angle.sinSnap(angrad); + double rCos = Angle.cosSnap(angrad); + + assertEquals(rSin, Math.sin(angrad)); + assertEquals(rCos, Math.cos(angrad)); + + } + } + private static Coordinate p(double x, double y) { return new Coordinate(x, y); } diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/BufferTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/BufferTest.java index 73b5509362..6e32380d8d 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/BufferTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/BufferTest.java @@ -11,6 +11,7 @@ */ package org.locationtech.jts.operation.buffer; +import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryCollection; import org.locationtech.jts.geom.GeometryFactory; @@ -602,4 +603,22 @@ private boolean hasHole(Geometry geom) { return false; } + /** + * See GEOS PR https://github.com/libgeos/geos/pull/978 + */ + public void testDefaultBuffer() { + Geometry g = read("POINT (0 0)").buffer(1.0); + Geometry b = g.getBoundary(); + Coordinate[] coords = b.getCoordinates(); + assertEquals(33, coords.length); + assertEquals(coords[0].x, 1.0); + assertEquals(coords[0].y, 0.0); + assertEquals(coords[8].x, 0.0); + assertEquals(coords[8].y, -1.0); + assertEquals(coords[16].x, -1.0); + assertEquals(coords[16].y, 0.0); + assertEquals(coords[24].x, 0.0); + assertEquals(coords[24].y, 1.0); + } + } From e25f8d9d32bbbeff0b12d656a57fbcafea4f866a Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 15 Nov 2023 15:55:57 -0800 Subject: [PATCH 036/123] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 6b45106661..1fcdfbbc38 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -37,6 +37,7 @@ Distributions for older JTS versions can be obtained at the * Reduce buffer curve short fillet segments (#960) * Added ability to specify boundary for `LargestEmptyCircle` (#973) * Improve `DouglaPeuckerSimplifier` and `TopologyPreservingSimplifier` to handle ring endpoints (#1013) +* Add Angle.sinSnap and .cosSnap to avoid small errors, e.g. with buffer operations (#1016) ### Bug Fixes * Fix `PreparedGeometry` handling of EMPTY elements (#904) From 89bfbabf8852f9bd631225644ef0d2734e092ae8 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 21 Nov 2023 15:59:12 -0800 Subject: [PATCH 037/123] Improve Buffer ring simplfication (#1022) --- .../buffer/BufferInputLineSimplifier.java | 65 ++++++++----------- .../jts/operation/buffer/BufferTest.java | 64 +++++++++++++----- 2 files changed, 74 insertions(+), 55 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferInputLineSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferInputLineSimplifier.java index 036f818e02..6dc899028b 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferInputLineSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferInputLineSimplifier.java @@ -14,18 +14,20 @@ import org.locationtech.jts.algorithm.Distance; import org.locationtech.jts.algorithm.Orientation; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateArrays; import org.locationtech.jts.geom.CoordinateList; /** * Simplifies a buffer input line to * remove concavities with shallow depth. *

- * The most important benefit of doing this + * The major benefit of doing this * is to reduce the number of points and the complexity of * shape which will be buffered. + * This improve performance and robustness. * It also reduces the risk of gores created by * the quantized fillet arcs (although this issue - * should be eliminated in any case by the + * should be eliminated by the * offset curve generation logic). *

* A key aspect of the simplification is that it @@ -35,8 +37,9 @@ * lies at the correct distance from the input geometry. *

* Another important heuristic used is that the end segments - * of the input are never simplified. This ensures that + * of linear inputs are never simplified. This ensures that * the client buffer code is able to generate end caps faithfully. + * Ring inputs can have end segments removed by simplification. *

* No attempt is made to avoid self-intersections in the output. * This is acceptable for use for generating a buffer offset curve, @@ -67,18 +70,18 @@ public static Coordinate[] simplify(Coordinate[] inputLine, double distanceTol) return simp.simplify(distanceTol); } - private static final int INIT = 0; private static final int DELETE = 1; - private static final int KEEP = 1; - private Coordinate[] inputLine; private double distanceTol; - private byte[] isDeleted; + private boolean isRing; + private boolean[] isDeleted; private int angleOrientation = Orientation.COUNTERCLOCKWISE; + public BufferInputLineSimplifier(Coordinate[] inputLine) { this.inputLine = inputLine; + isRing = CoordinateArrays.isRing(inputLine); } /** @@ -94,11 +97,12 @@ public BufferInputLineSimplifier(Coordinate[] inputLine) { public Coordinate[] simplify(double distanceTol) { this.distanceTol = Math.abs(distanceTol); + angleOrientation = Orientation.COUNTERCLOCKWISE; if (distanceTol < 0) angleOrientation = Orientation.CLOCKWISE; - // rely on fact that boolean array is filled with false value - isDeleted = new byte[inputLine.length]; + // rely on fact that boolean array is filled with false values + isDeleted = new boolean[inputLine.length]; boolean isChanged = false; do { @@ -112,18 +116,19 @@ public Coordinate[] simplify(double distanceTol) * Uses a sliding window containing 3 vertices to detect shallow angles * in which the middle vertex can be deleted, since it does not * affect the shape of the resulting buffer in a significant way. - * @return + * + * @return true if any vertices were deleted */ private boolean deleteShallowConcavities() { /** - * Do not simplify end line segments of the line string. + * Do not simplify end line segments of lines. * This ensures that end caps are generated consistently. */ - int index = 1; + int index = isRing ? 0 : 1; - int midIndex = findNextNonDeletedIndex(index); - int lastIndex = findNextNonDeletedIndex(midIndex); + int midIndex = nextIndex(index); + int lastIndex = nextIndex(midIndex); boolean isChanged = false; while (lastIndex < inputLine.length) { @@ -131,7 +136,7 @@ private boolean deleteShallowConcavities() boolean isMiddleVertexDeleted = false; if (isDeletable(index, midIndex, lastIndex, distanceTol)) { - isDeleted[midIndex] = DELETE; + isDeleted[midIndex] = true; isMiddleVertexDeleted = true; isChanged = true; } @@ -141,8 +146,8 @@ private boolean deleteShallowConcavities() else index = midIndex; - midIndex = findNextNonDeletedIndex(index); - lastIndex = findNextNonDeletedIndex(midIndex); + midIndex = nextIndex(index); + lastIndex = nextIndex(midIndex); } return isChanged; } @@ -153,10 +158,10 @@ private boolean deleteShallowConcavities() * @return the next non-deleted index, if any * or inputLine.length if there are no more non-deleted indices */ - private int findNextNonDeletedIndex(int index) + private int nextIndex(int index) { int next = index + 1; - while (next < inputLine.length && isDeleted[next] == DELETE) + while (next < inputLine.length && isDeleted[next]) next++; return next; } @@ -165,10 +170,9 @@ private Coordinate[] collapseLine() { CoordinateList coordList = new CoordinateList(); for (int i = 0; i < inputLine.length; i++) { - if (isDeleted[i] != DELETE) + if (! isDeleted[i]) coordList.add(inputLine[i]); } -// if (coordList.size() < inputLine.length) System.out.println("Simplified " + (inputLine.length - coordList.size()) + " pts"); return coordList.toCoordinateArray(); } @@ -181,22 +185,8 @@ private boolean isDeletable(int i0, int i1, int i2, double distanceTol) if (! isConcave(p0, p1, p2)) return false; if (! isShallow(p0, p1, p2, distanceTol)) return false; - // MD - don't use this heuristic - it's too restricting -// if (p0.distance(p2) > distanceTol) return false; - return isShallowSampled(p0, p1, i0, i2, distanceTol); } - - private boolean isShallowConcavity(Coordinate p0, Coordinate p1, Coordinate p2, double distanceTol) - { - int orientation = Orientation.index(p0, p1, p2); - boolean isAngleToSimplify = (orientation == angleOrientation); - if (! isAngleToSimplify) - return false; - - double dist = Distance.pointToSegment(p1, p0, p2); - return dist < distanceTol; - } private static final int NUM_PTS_TO_CHECK = 10; @@ -219,18 +209,17 @@ private boolean isShallowSampled(Coordinate p0, Coordinate p2, int i0, int i2, d if (inc <= 0) inc = 1; for (int i = i0; i < i2; i += inc) { - if (! isShallow(p0, p2, inputLine[i], distanceTol)) return false; + if (! isShallow(p0, inputLine[i], p2, distanceTol)) return false; } return true; } - private boolean isShallow(Coordinate p0, Coordinate p1, Coordinate p2, double distanceTol) + private static boolean isShallow(Coordinate p0, Coordinate p1, Coordinate p2, double distanceTol) { double dist = Distance.pointToSegment(p1, p0, p2); return dist < distanceTol; } - private boolean isConcave(Coordinate p0, Coordinate p1, Coordinate p2) { int orientation = Orientation.index(p0, p1, p2); diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/BufferTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/BufferTest.java index 6e32380d8d..ab59b70567 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/BufferTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/BufferTest.java @@ -575,8 +575,54 @@ public void testLineClosedNoHole() { checkBufferHasHole(wkt, 70, false); } + /** + * See GEOS PR https://github.com/libgeos/geos/pull/978 + */ + public void testDefaultBuffer() { + Geometry g = read("POINT (0 0)").buffer(1.0); + Geometry b = g.getBoundary(); + Coordinate[] coords = b.getCoordinates(); + assertEquals(33, coords.length); + assertEquals(coords[0].x, 1.0); + assertEquals(coords[0].y, 0.0); + assertEquals(coords[8].x, 0.0); + assertEquals(coords[8].y, -1.0); + assertEquals(coords[16].x, -1.0); + assertEquals(coords[16].y, 0.0); + assertEquals(coords[24].x, 0.0); + assertEquals(coords[24].y, 1.0); + } + + public void testRingStartSimplified() { + checkBuffer("POLYGON ((200 300, 200 299.9999, 350 100, 30 40, 200 300))", + 20, bufParamRoundMitre(5), + "POLYGON ((198.88 334.83, 385.3 86.27, -12.4 11.7, 198.88 334.83))" + ); + } + + public void testRingEndSimplified() { + checkBuffer("POLYGON ((200 300, 350 100, 30 40, 200 299.9999, 200 300))", + 20, bufParamRoundMitre(5), + "POLYGON ((198.88 334.83, 385.3 86.27, -12.4 11.7, 198.88 334.83))" + ); + } + //=================================================== + private static BufferParameters bufParamRoundMitre(double mitreLimit) { + BufferParameters param = new BufferParameters(); + param.setJoinStyle(BufferParameters.JOIN_MITRE); + param.setMitreLimit(mitreLimit); + return param; + } + + private void checkBuffer(String wkt, double dist, BufferParameters param, String wktExpected) { + Geometry geom = read(wkt); + Geometry result = BufferOp.bufferOp(geom, dist, param); + Geometry expected = read(wktExpected); + checkEqual(expected, result, 0.01); + } + private void checkBufferEmpty(String wkt, double dist, boolean isEmptyExpected) { Geometry a = read(wkt); Geometry result = a.buffer(dist); @@ -603,22 +649,6 @@ private boolean hasHole(Geometry geom) { return false; } - /** - * See GEOS PR https://github.com/libgeos/geos/pull/978 - */ - public void testDefaultBuffer() { - Geometry g = read("POINT (0 0)").buffer(1.0); - Geometry b = g.getBoundary(); - Coordinate[] coords = b.getCoordinates(); - assertEquals(33, coords.length); - assertEquals(coords[0].x, 1.0); - assertEquals(coords[0].y, 0.0); - assertEquals(coords[8].x, 0.0); - assertEquals(coords[8].y, -1.0); - assertEquals(coords[16].x, -1.0); - assertEquals(coords[16].y, 0.0); - assertEquals(coords[24].x, 0.0); - assertEquals(coords[24].y, 1.0); - } + } From 85c9391a0446f63208027697b3948b668c2e38b0 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 21 Nov 2023 15:59:58 -0800 Subject: [PATCH 038/123] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 1fcdfbbc38..2b60509a6b 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -38,6 +38,7 @@ Distributions for older JTS versions can be obtained at the * Added ability to specify boundary for `LargestEmptyCircle` (#973) * Improve `DouglaPeuckerSimplifier` and `TopologyPreservingSimplifier` to handle ring endpoints (#1013) * Add Angle.sinSnap and .cosSnap to avoid small errors, e.g. with buffer operations (#1016) +* Improve Buffer input simplification for rings (#1022) ### Bug Fixes * Fix `PreparedGeometry` handling of EMPTY elements (#904) From 5b1bb50190663521f6c27d8355e60795b2e66cfd Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 22 Nov 2023 12:33:59 -0800 Subject: [PATCH 039/123] Remove unused code --- .../jts/algorithm/distance/DiscreteHausdorffDistance.java | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/distance/DiscreteHausdorffDistance.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/distance/DiscreteHausdorffDistance.java index b1512d4449..a45cbcbae9 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/distance/DiscreteHausdorffDistance.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/distance/DiscreteHausdorffDistance.java @@ -140,7 +140,6 @@ public static class MaxPointDistanceFilter { private PointPairDistance maxPtDist = new PointPairDistance(); private PointPairDistance minPtDist = new PointPairDistance(); - private DistanceToPoint euclideanDist = new DistanceToPoint(); private Geometry geom; public MaxPointDistanceFilter(Geometry geom) From dab54e34e6dde50289fc154739d6a210563d4460 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 29 Nov 2023 10:29:12 -0800 Subject: [PATCH 040/123] Fix InteriorPoint for MultiLineString with EMPTY (#1023) --- .../jts/algorithm/InteriorPointLine.java | 6 ++++++ .../jts/algorithm/InteriorPointTest.java | 4 ++++ .../testxml/general/TestInteriorPoint.xml | 20 ++++++++++++++++--- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPointLine.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPointLine.java index ca5a9832d4..0594a4817f 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPointLine.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPointLine.java @@ -69,6 +69,9 @@ public Coordinate getInteriorPoint() */ private void addInterior(Geometry geom) { + if (geom.isEmpty()) + return; + if (geom instanceof LineString) { addInterior(geom.getCoordinates()); } @@ -93,6 +96,9 @@ private void addInterior(Coordinate[] pts) */ private void addEndpoints(Geometry geom) { + if (geom.isEmpty()) + return; + if (geom instanceof LineString) { addEndpoints(geom.getCoordinates()); } diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/InteriorPointTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/InteriorPointTest.java index bed933f7fe..52aee7761d 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/InteriorPointTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/InteriorPointTest.java @@ -50,6 +50,10 @@ public void testPolygonZeroArea() { checkInteriorPoint(read("POLYGON ((10 10, 10 10, 10 10, 10 10))"), new Coordinate(10, 10)); } + public void testMultiLineWithEmpty() { + checkInteriorPoint(read("MULTILINESTRING ((0 0, 1 1), EMPTY)"), new Coordinate(0, 0)); + } + public void testAll() throws Exception { checkInteriorPointFile(TestFiles.getResourceFilePath("world.wkt")); diff --git a/modules/tests/src/test/resources/testxml/general/TestInteriorPoint.xml b/modules/tests/src/test/resources/testxml/general/TestInteriorPoint.xml index 9c3cbdc05b..b6450dbd0f 100644 --- a/modules/tests/src/test/resources/testxml/general/TestInteriorPoint.xml +++ b/modules/tests/src/test/resources/testxml/general/TestInteriorPoint.xml @@ -1,5 +1,4 @@ - P - empty @@ -38,7 +37,7 @@ L - linestring with single segment LINESTRING (0 0, 7 14) - POINT (7 14) + POINT (0 0) @@ -68,6 +67,13 @@ POINT (180 200) + + mL - multilinestring with empty + MULTILINESTRING ((0 0, 1 1), EMPTY) + + POINT (0 0) + + A - box POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0)) @@ -93,7 +99,7 @@ A - polygon with horizontal segment at centre (narrower L shape) POLYGON ((0 2, 0 4, 3 4, 3 0, 2 0, 2 2, 0 2)) - POINT (2 3) + POINT (1.5 3) @@ -103,6 +109,14 @@ POINT (115 200) + + + mA - multipolygon with empty + MULTIPOLYGON (((0 2, 0 4, 3 4, 3 0, 2 0, 2 2, 0 2)), EMPTY) + + POINT (1.5 3) + + GC - collection of polygons, lines, points GEOMETRYCOLLECTION (POLYGON ((0 40, 40 40, 40 0, 0 0, 0 40)), From ac92f411011a04383f77d6ad5db4aa8c2480f8b4 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 29 Nov 2023 10:30:20 -0800 Subject: [PATCH 041/123] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 2b60509a6b..ab6a54950a 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -66,6 +66,7 @@ Distributions for older JTS versions can be obtained at the * Fix `InteriorPointPoint` to handle empty elements * Fix `DistanceOp` for empty elements (#1010) * Fix predicates for MultiPoint with EMPTY (#1015) +* Fix `InteriorPoint` for MultiLineString with EMPTY (#1023) ### Performance Improvements From cbd7086d964d870bf6f040efd12493ddcc055947 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 29 Nov 2023 11:51:53 -0800 Subject: [PATCH 042/123] Renaming InteriorPoint internal objects --- .../org/locationtech/jts/algorithm/InteriorPoint.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPoint.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPoint.java index 47b411c26b..75282a838b 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPoint.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPoint.java @@ -59,8 +59,7 @@ public static Coordinate getInteriorPoint(Geometry geom) { return null; Coordinate interiorPt = null; - //int dim = geom.getDimension(); - int dim = effectiveDimension(geom); + int dim = dimensionNonEmpty(geom); // this should not happen, but just in case... if (dim < 0) { return null; @@ -77,13 +76,13 @@ else if (dim == 1) { return interiorPt; } - private static int effectiveDimension(Geometry geom) { - EffectiveDimensionFilter dimFilter = new EffectiveDimensionFilter(); + private static int dimensionNonEmpty(Geometry geom) { + DimensionNonEmptyFilter dimFilter = new DimensionNonEmptyFilter(); geom.apply(dimFilter); return dimFilter.getDimension(); } - private static class EffectiveDimensionFilter implements GeometryFilter + private static class DimensionNonEmptyFilter implements GeometryFilter { private int dim = -1; From df3a4de8d3890dcd0ccefb63a703492cdf53c351 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 29 Nov 2023 15:58:38 -0800 Subject: [PATCH 043/123] Add IsSimple tests for empty elements --- .../resources/testxml/general/TestSimple.xml | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/modules/tests/src/test/resources/testxml/general/TestSimple.xml b/modules/tests/src/test/resources/testxml/general/TestSimple.xml index 539cc00c78..a75cdb1f81 100644 --- a/modules/tests/src/test/resources/testxml/general/TestSimple.xml +++ b/modules/tests/src/test/resources/testxml/general/TestSimple.xml @@ -49,6 +49,14 @@ + + mP - with empty element + + MULTIPOINT (EMPTY, (80 220), (160 220)) + + true + + L - simple line @@ -349,6 +357,14 @@ + + mL - with empty element + + MULTILINESTRING ((0 0, 100 100), EMPTY) + + true + + LR - valid ring @@ -453,6 +469,14 @@ MULTIPOLYGON (((100 100, 100 200, 200 100, 200 200, 100 100)), ((100 400, 200 40 + + mA - with empty element + +MULTIPOLYGON (((0 10, 10 10, 10 0, 0 0, 0 10)), EMPTY) + + true + + GC - all components simple @@ -477,4 +501,13 @@ GEOMETRYCOLLECTION (POLYGON ((100 100, 100 200, 200 100, 200 200, 100 100)), + + GC - with empty element + +GEOMETRYCOLLECTION (POLYGON ((0 10, 10 10, 10 0, 0 0, 0 10)), + LINESTRING (100 300, 200 250), + POINT EMPTY) + true + + From 8b73d847413241e1c952fadd9c9ba18e63f267b2 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Fri, 1 Dec 2023 11:43:04 -0800 Subject: [PATCH 044/123] Improve TestBuilder layout - shorter tab names --- .../org/locationtech/jtstest/testbuilder/TestCasePanel.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/TestCasePanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/TestCasePanel.java index dca7dc145f..b47ae1c91c 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/TestCasePanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/TestCasePanel.java @@ -356,9 +356,9 @@ public void actionPerformed(ActionEvent e) { tabFunctions.add(spatialFunctionPanel, "Geometry"); tabFunctions.add(scalarFunctionPanel, "Scalar"); - jTabbedPane1.add(tabFunctions, "Functions"); - jTabbedPane1.add(relateTabPanel, "Predicates"); - jTabbedPane1.add(validPanel, "Valid / Mark"); + jTabbedPane1.add(tabFunctions, "Function"); + jTabbedPane1.add(relateTabPanel, "Predicate"); + jTabbedPane1.add(validPanel, "Valid/Mark"); relateTabPanel.add(relatePanel, BorderLayout.CENTER); From caf3a2805f044abf768f57bdb0f23605a843e956 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Fri, 1 Dec 2023 13:09:53 -0800 Subject: [PATCH 045/123] Add TestBuilder Valid target geometry UI --- .../jtstest/testbuilder/AppColors.java | 4 + .../jtstest/testbuilder/ValidPanel.java | 134 +++++++++++++----- 2 files changed, 101 insertions(+), 37 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppColors.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppColors.java index 812984c6d2..e70aa2bf9a 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppColors.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppColors.java @@ -16,6 +16,10 @@ public class AppColors { + public static final Color A = Color.RED; + public static final Color B = Color.BLUE; + public static final Color Result = Color.GREEN; + public static final Color BACKGROUND = UIManager.getColor ( "Panel.background" ); public static final Color BACKGROUND_ERROR = Color.PINK; public static final Color TAB_FOCUS = UIManager.getColor ("TabbedPane.highlight" ); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/ValidPanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/ValidPanel.java index e9bf9a801e..c8ed635e24 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/ValidPanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/ValidPanel.java @@ -14,22 +14,29 @@ import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; -import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; -import java.awt.Insets; import java.awt.event.ActionEvent; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; import java.util.Vector; + import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JPanel; +import javax.swing.JRadioButton; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.SwingConstants; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.io.WKTWriter; import org.locationtech.jts.operation.valid.IsSimpleOp; import org.locationtech.jts.operation.valid.IsValidOp; @@ -54,7 +61,7 @@ public class ValidPanel extends JPanel { JTextField txtIsSimple = new JTextField(); JTextArea taInvalidMsg = new JTextArea(); JLabel lblValidSimple = new JLabel(); - JPanel jPanel1 = new JPanel(); + JPanel panelValidSimple = new JPanel(); private transient Vector validPanelListeners; GridLayout gridLayout1 = new GridLayout(); JPanel markPanel = new JPanel(); @@ -66,6 +73,9 @@ public class ValidPanel extends JPanel { JButton btnClearMark = new JButton(); JButton btnSetMark = new JButton(); private JCheckBox cbInvertedRingAllowed; + JRadioButton rbA = new JRadioButton(); + JRadioButton rbB = new JRadioButton(); + JRadioButton rbResult = new JRadioButton(); public ValidPanel() { try { @@ -98,7 +108,47 @@ public void actionPerformed(ActionEvent e) { clearAll(); } }); + + rbA.setSelected(true); + rbA.setText("A"); + rbA.setForeground(AppColors.A); + rbB.setText("B"); + rbB.setForeground(AppColors.B); + rbResult.setText("Result"); + rbA.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (e.getStateChange() == ItemEvent.SELECTED) { + clearAll(); + } + }}); + rbB.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (e.getStateChange() == ItemEvent.SELECTED) { + clearAll(); + } + }}); + rbResult.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (e.getStateChange() == ItemEvent.SELECTED) { + clearAll(); + } + }}); + + + ButtonGroup btnGrpStdInFormat = new ButtonGroup(); + btnGrpStdInFormat.add(rbA); + btnGrpStdInFormat.add(rbB); + btnGrpStdInFormat.add(rbResult); + + JPanel panelABR = new JPanel(); + panelABR.setLayout(new BoxLayout(panelABR, BoxLayout.X_AXIS)); + panelABR.add(rbA); + panelABR.add(rbB); + panelABR.add(rbResult); cbInvertedRingAllowed = new JCheckBox(); cbInvertedRingAllowed.setToolTipText(AppStrings.TIP_ALLOW_INVERTED_RINGS); @@ -152,9 +202,9 @@ public void actionPerformed(ActionEvent e) { } }); - JPanel panelValidSimple = new JPanel(); - panelValidSimple.add(btnValidate); - panelValidSimple.add(txtIsValid); + JPanel panelValid = new JPanel(); + panelValid.add(btnValidate); + panelValid.add(txtIsValid); JPanel panelSimple = new JPanel(); panelSimple.add(btnSimple); @@ -164,28 +214,18 @@ public void actionPerformed(ActionEvent e) { panelMsg.add(taInvalidMsg); JPanel panelClear = new JPanel(); - panelClear.add(btnClear); + panelClear.setLayout(new BorderLayout()); + panelClear.add(cbInvertedRingAllowed, BorderLayout.WEST); + panelClear.add(btnClear, BorderLayout.EAST); - jPanel1.setLayout(new GridBagLayout()); - jPanel1.add(panelSimple, new GridBagConstraints(0, 1, 2, 1, 0.0, 0.0 - ,GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(10, 5, 10, 5), 0, 0)); - jPanel1.add(panelValidSimple, new GridBagConstraints(0, 2, 2, 1, 0.0, 0.0 - ,GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(10, 5, 10, 5), 0, 0)); - jPanel1.add(cbInvertedRingAllowed, new GridBagConstraints(0, 3, 2, 1, 1.0, 0.0 - ,GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(4, 0, 4, 0), 10, 0)); + panelValidSimple.setLayout(new BoxLayout(panelValidSimple, BoxLayout.Y_AXIS)); + panelValidSimple.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + panelValidSimple.add(panelABR); + panelValidSimple.add(panelSimple); + panelValidSimple.add(panelValid); + panelValidSimple.add(panelClear); + panelValidSimple.add(panelMsg); - /*jPanel1.add(lblValidSimple, new GridBagConstraints(0, 4, 1, 1, 1.0, 0.0 - ,GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(0, 4, 0, 4), 0, 0)); -*/ - /* - jPanel1.add(txtIsValid, new GridBagConstraints(1, 4, 1, 1, 1.0, 0.0 - ,GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(4, 0, 4, 0), 10, 0)); - */ - jPanel1.add(panelMsg, new GridBagConstraints(0, 4, 2, 1, 0.0, 0.0 - ,GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 4, 0, 4), 0, 0)); - jPanel1.add(panelClear, new GridBagConstraints(0, 5, 2, 1, 0.0, 0.0 - ,GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 4, 0, 4), 0, 0)); - //---------------------------------------------- txtMarkLocation.setBorder(BorderFactory.createLoweredBevelBorder()); txtMarkLocation.setToolTipText(""); @@ -209,7 +249,7 @@ public void actionPerformed(ActionEvent e) { //---------------------------------------------- this.setLayout(new BorderLayout()); - this.add(jPanel1, BorderLayout.CENTER); + this.add(panelValidSimple, BorderLayout.NORTH); this.add(markPanel, BorderLayout.SOUTH); } @@ -228,14 +268,12 @@ void clearAll() { void btnValidate_actionPerformed(ActionEvent e) { - TopologyValidationError err = null; - if (testCase.getGeometry(0) != null) { - IsValidOp validOp = new IsValidOp(testCase.getGeometry(0)); - if (cbInvertedRingAllowed.isSelected()) { - validOp.setSelfTouchingRingFormingHoleValid(true); - } - err = validOp.getValidationError(); - } + clearFlag(txtIsValid); + Geometry geom = getGeometry(); + if (geom == null) + return; + + TopologyValidationError err = checkValid(geom, cbInvertedRingAllowed.isSelected()); String msg = ""; boolean isValid = true; Coordinate invalidPoint = null; @@ -248,12 +286,34 @@ void btnValidate_actionPerformed(ActionEvent e) setFlagText(txtIsValid, isValid); setMarkPoint(invalidPoint); } + + private TopologyValidationError checkValid(Geometry geom, boolean isAllowInverted) { + TopologyValidationError err = null; + if (geom != null) { + IsValidOp validOp = new IsValidOp(geom); + if (isAllowInverted) { + validOp.setSelfTouchingRingFormingHoleValid(true); + } + err = validOp.getValidationError(); + } + return err; + } + + private Geometry getGeometry() { + if (rbA.isSelected()) + return testCase.getGeometry(0); + if (rbB.isSelected()) + return testCase.getGeometry(1); + return testCase.getResult(); + } + void btnSimple_actionPerformed(ActionEvent e) { boolean isSimple = true; Coordinate nonSimpleLoc = null; - if (testCase.getGeometry(0) != null) { - IsSimpleOp simpleOp = new IsSimpleOp(testCase.getGeometry(0)); + Geometry geom = getGeometry(); + if (geom != null) { + IsSimpleOp simpleOp = new IsSimpleOp(geom); isSimple = simpleOp.isSimple(); nonSimpleLoc = simpleOp.getNonSimpleLocation(); } From 2bde55fd30e9f3012ec152e85c3489ef54b89530 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Sat, 2 Dec 2023 12:33:13 -0800 Subject: [PATCH 046/123] Use constant for geometry colors --- .../jtstest/testbuilder/AppColors.java | 4 ++-- .../testbuilder/GeometryInputDialog.java | 8 +++---- .../jtstest/testbuilder/InspectorPanel.java | 2 +- .../jtstest/testbuilder/RelatePanel.java | 24 +++++++++---------- .../jtstest/testbuilder/WKTPanel.java | 4 ++-- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppColors.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppColors.java index e70aa2bf9a..10265f3e60 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppColors.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppColors.java @@ -16,8 +16,8 @@ public class AppColors { - public static final Color A = Color.RED; - public static final Color B = Color.BLUE; + public static final Color A = Color.BLUE; + public static final Color B = Color.RED; public static final Color Result = Color.GREEN; public static final Color BACKGROUND = UIManager.getColor ( "Panel.background" ); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryInputDialog.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryInputDialog.java index 2b4d5b8048..dcab3c4412 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryInputDialog.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryInputDialog.java @@ -80,7 +80,7 @@ void jbInit() throws Exception { border1 = BorderFactory.createLineBorder(Color.gray, 2); panel1.setLayout(borderLayout1); jLabel1.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel1.setForeground(Color.blue); + jLabel1.setForeground(AppColors.A); jLabel1.setToolTipText(""); jLabel1.setText("A"); jPanel1.setLayout(gridBagLayout2); @@ -100,7 +100,7 @@ public void actionPerformed(ActionEvent e) { } }); jLabel2.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel2.setForeground(Color.red); + jLabel2.setForeground(AppColors.B); jLabel2.setText("B"); lblError.setToolTipText(""); txtError.setLineWrap(true); @@ -211,9 +211,9 @@ void btnCancel_actionPerformed(ActionEvent e) { void btnLoad_actionPerformed(ActionEvent e) { parseError = false; - geom[0] = parseGeometry(txtA, Color.blue); + geom[0] = parseGeometry(txtA, AppColors.A); if (!parseError) - geom[1] = parseGeometry(txtB, Color.red); + geom[1] = parseGeometry(txtB, AppColors.B); if (!parseError) setVisible(false); } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/InspectorPanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/InspectorPanel.java index 62fd8f2162..1504770125 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/InspectorPanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/InspectorPanel.java @@ -199,7 +199,7 @@ public void setGeometry(String tag, Geometry geom, int source, boolean isEditabl btnDelete.setEnabled(isEditable); lblGeom.setText(tag); lblGeom.setToolTipText(tag); - lblGeom.setForeground(source == 0 ? Color.BLUE : Color.RED); + lblGeom.setForeground(source == 0 ? AppColors.A : AppColors.B); sortNone(); } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/RelatePanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/RelatePanel.java index 7b2dee2a89..5314f0ef46 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/RelatePanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/RelatePanel.java @@ -263,20 +263,20 @@ void jbInit() throws Exception { relateIB.setText("F"); jPanel1.setLayout(gridBagLayout2); jLabel14.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel14.setForeground(Color.blue); + jLabel14.setForeground(AppColors.A); jLabel14.setText("A"); jLabel13.setFont(new java.awt.Font("Dialog", 2, 12)); - jLabel13.setForeground(Color.blue); + jLabel13.setForeground(AppColors.A); jLabel13.setText("Ext"); jLabel12.setFont(new java.awt.Font("Dialog", 2, 12)); - jLabel12.setForeground(Color.blue); + jLabel12.setForeground(AppColors.A); jLabel12.setText("Bdy"); jLabel11.setFont(new java.awt.Font("Dialog", 2, 12)); - jLabel11.setForeground(Color.blue); + jLabel11.setForeground(AppColors.A); jLabel11.setToolTipText(""); jLabel11.setText("Int"); jLabel10.setFont(new java.awt.Font("Dialog", 2, 12)); - jLabel10.setForeground(Color.red); + jLabel10.setForeground(AppColors.B); jLabel10.setToolTipText(""); jLabel10.setText("Ext"); txtAB.setBackground(AppColors.BACKGROUND); @@ -288,23 +288,23 @@ void jbInit() throws Exception { txtAB.setEditable(false); txtAB.setHorizontalAlignment(SwingConstants.LEFT); jLabel23.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel23.setForeground(Color.red); + jLabel23.setForeground(AppColors.B); jLabel23.setText("B"); relateBI.setFont(new java.awt.Font("Dialog", 1, 12)); relateBI.setText("F"); jLabel22.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel22.setForeground(Color.blue); + jLabel22.setForeground(AppColors.A); jLabel22.setToolTipText(""); jLabel22.setText("A"); relateEI.setFont(new java.awt.Font("Dialog", 1, 12)); relateEI.setText("F"); jLabel21.setToolTipText(""); jLabel21.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel21.setForeground(Color.blue); + jLabel21.setForeground(AppColors.A); jLabel21.setToolTipText(""); jLabel21.setText("A"); jLabel20.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel20.setForeground(Color.red); + jLabel20.setForeground(AppColors.B); jLabel20.setToolTipText(""); jLabel20.setText("B"); relateBE.setFont(new java.awt.Font("Dialog", 1, 12)); @@ -321,17 +321,17 @@ void jbInit() throws Exception { relateBB.setFont(new java.awt.Font("Dialog", 1, 12)); relateBB.setText("F"); jLabel9.setFont(new java.awt.Font("Dialog", 2, 12)); - jLabel9.setForeground(Color.red); + jLabel9.setForeground(AppColors.B); jLabel9.setToolTipText(""); jLabel9.setText("Bdy"); relateEB.setFont(new java.awt.Font("Dialog", 1, 12)); relateEB.setText("F"); jLabel8.setFont(new java.awt.Font("Dialog", 2, 12)); - jLabel8.setForeground(Color.red); + jLabel8.setForeground(AppColors.B); jLabel8.setToolTipText(""); jLabel8.setText("Int"); jLabel7.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel7.setForeground(Color.red); + jLabel7.setForeground(AppColors.B); jLabel7.setText("B"); relateII.setBackground(Color.white); relateII.setFont(new java.awt.Font("Dialog", 1, 12)); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/WKTPanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/WKTPanel.java index 7b339ecbc2..6e9bd43304 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/WKTPanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/WKTPanel.java @@ -150,13 +150,13 @@ public void actionPerformed(ActionEvent e) { panelAB.setLayout(gridBagLayout2); aLabel.setFont(new java.awt.Font("Dialog", 1, 16)); - aLabel.setForeground(Color.blue); + aLabel.setForeground(AppColors.A); aLabel.setText("A"); aLabel.setPreferredSize(new Dimension(20, 20)); aLabel.setHorizontalTextPosition(SwingConstants.LEFT); bLabel.setFont(new java.awt.Font("Dialog", 1, 16)); - bLabel.setForeground(Color.red); + bLabel.setForeground(AppColors.B); bLabel.setText("B"); bLabel.setPreferredSize(new Dimension(20, 20)); From 7d852cca24e6028e2996f17b8f416e033e469bbc Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Sat, 2 Dec 2023 12:46:36 -0800 Subject: [PATCH 047/123] Add constants for geometry labels --- .../locationtech/jtstest/testbuilder/AppStrings.java | 1 + .../jtstest/testbuilder/GeometryInputDialog.java | 4 ++-- .../jtstest/testbuilder/RelatePanel.java | 12 ++++++------ .../jtstest/testbuilder/TestCaseTextDialog.java | 6 +++--- .../locationtech/jtstest/testbuilder/ValidPanel.java | 6 +++--- .../locationtech/jtstest/testbuilder/WKTPanel.java | 4 ++-- .../jtstest/testbuilder/model/LayerList.java | 7 ++++--- 7 files changed, 21 insertions(+), 19 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppStrings.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppStrings.java index 9c8ff00154..13b62e0473 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppStrings.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppStrings.java @@ -18,6 +18,7 @@ public class AppStrings { public static final String GEOM_LABEL_A = "A"; public static final String GEOM_LABEL_B = "B"; + public static final String GEOM_LABEL_RESULT = "Result"; public static final String TAB_LABEL_LOG = "Log"; public static final String TAB_LABEL_VALUE = "Value"; diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryInputDialog.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryInputDialog.java index dcab3c4412..bc8ba79800 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryInputDialog.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryInputDialog.java @@ -82,7 +82,7 @@ void jbInit() throws Exception { jLabel1.setFont(new java.awt.Font("Dialog", 1, 12)); jLabel1.setForeground(AppColors.A); jLabel1.setToolTipText(""); - jLabel1.setText("A"); + jLabel1.setText(AppStrings.GEOM_LABEL_A); jPanel1.setLayout(gridBagLayout2); btnLoad.setToolTipText(""); btnLoad.setText("Load"); @@ -101,7 +101,7 @@ public void actionPerformed(ActionEvent e) { }); jLabel2.setFont(new java.awt.Font("Dialog", 1, 12)); jLabel2.setForeground(AppColors.B); - jLabel2.setText("B"); + jLabel2.setText(AppStrings.GEOM_LABEL_B); lblError.setToolTipText(""); txtError.setLineWrap(true); txtError.setBorder(BorderFactory.createEtchedBorder()); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/RelatePanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/RelatePanel.java index 5314f0ef46..4db24c3500 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/RelatePanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/RelatePanel.java @@ -264,7 +264,7 @@ void jbInit() throws Exception { jPanel1.setLayout(gridBagLayout2); jLabel14.setFont(new java.awt.Font("Dialog", 1, 12)); jLabel14.setForeground(AppColors.A); - jLabel14.setText("A"); + jLabel14.setText(AppStrings.GEOM_LABEL_A); jLabel13.setFont(new java.awt.Font("Dialog", 2, 12)); jLabel13.setForeground(AppColors.A); jLabel13.setText("Ext"); @@ -289,24 +289,24 @@ void jbInit() throws Exception { txtAB.setHorizontalAlignment(SwingConstants.LEFT); jLabel23.setFont(new java.awt.Font("Dialog", 1, 12)); jLabel23.setForeground(AppColors.B); - jLabel23.setText("B"); + jLabel23.setText(AppStrings.GEOM_LABEL_B); relateBI.setFont(new java.awt.Font("Dialog", 1, 12)); relateBI.setText("F"); jLabel22.setFont(new java.awt.Font("Dialog", 1, 12)); jLabel22.setForeground(AppColors.A); jLabel22.setToolTipText(""); - jLabel22.setText("A"); + jLabel22.setText(AppStrings.GEOM_LABEL_A); relateEI.setFont(new java.awt.Font("Dialog", 1, 12)); relateEI.setText("F"); jLabel21.setToolTipText(""); jLabel21.setFont(new java.awt.Font("Dialog", 1, 12)); jLabel21.setForeground(AppColors.A); jLabel21.setToolTipText(""); - jLabel21.setText("A"); + jLabel21.setText(AppStrings.GEOM_LABEL_A); jLabel20.setFont(new java.awt.Font("Dialog", 1, 12)); jLabel20.setForeground(AppColors.B); jLabel20.setToolTipText(""); - jLabel20.setText("B"); + jLabel20.setText(AppStrings.GEOM_LABEL_B); relateBE.setFont(new java.awt.Font("Dialog", 1, 12)); relateBE.setText("F"); relateEE.setFont(new java.awt.Font("Dialog", 1, 12)); @@ -332,7 +332,7 @@ void jbInit() throws Exception { jLabel8.setText("Int"); jLabel7.setFont(new java.awt.Font("Dialog", 1, 12)); jLabel7.setForeground(AppColors.B); - jLabel7.setText("B"); + jLabel7.setText(AppStrings.GEOM_LABEL_B); relateII.setBackground(Color.white); relateII.setFont(new java.awt.Font("Dialog", 1, 12)); relateII.setText("F"); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/TestCaseTextDialog.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/TestCaseTextDialog.java index a0f3737231..fa6355db13 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/TestCaseTextDialog.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/TestCaseTextDialog.java @@ -263,9 +263,9 @@ void rbGML_actionPerformed(ActionEvent e) { private void writeView(String a, String b, String result) { txtGeomView.setText(""); - writeViewGeometry("A", a); - writeViewGeometry("B", b); - writeViewGeometry("Result", result); + writeViewGeometry(AppStrings.GEOM_LABEL_A, a); + writeViewGeometry(AppStrings.GEOM_LABEL_B, b); + writeViewGeometry(AppStrings.GEOM_LABEL_RESULT, result); } private void writeViewGeometry(String tag, String str) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/ValidPanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/ValidPanel.java index c8ed635e24..6f4968c038 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/ValidPanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/ValidPanel.java @@ -110,11 +110,11 @@ public void actionPerformed(ActionEvent e) { }); rbA.setSelected(true); - rbA.setText("A"); + rbA.setText(AppStrings.GEOM_LABEL_A); rbA.setForeground(AppColors.A); - rbB.setText("B"); + rbB.setText(AppStrings.GEOM_LABEL_B); rbB.setForeground(AppColors.B); - rbResult.setText("Result"); + rbResult.setText(AppStrings.GEOM_LABEL_RESULT); rbA.addItemListener(new ItemListener() { @Override diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/WKTPanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/WKTPanel.java index 6e9bd43304..76e63960bd 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/WKTPanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/WKTPanel.java @@ -151,13 +151,13 @@ public void actionPerformed(ActionEvent e) { aLabel.setFont(new java.awt.Font("Dialog", 1, 16)); aLabel.setForeground(AppColors.A); - aLabel.setText("A"); + aLabel.setText(AppStrings.GEOM_LABEL_A); aLabel.setPreferredSize(new Dimension(20, 20)); aLabel.setHorizontalTextPosition(SwingConstants.LEFT); bLabel.setFont(new java.awt.Font("Dialog", 1, 16)); bLabel.setForeground(AppColors.B); - bLabel.setText("B"); + bLabel.setText(AppStrings.GEOM_LABEL_B); bLabel.setPreferredSize(new Dimension(20, 20)); aScrollPane.setBorder(BorderFactory.createLoweredBevelBorder()); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/LayerList.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/LayerList.java index ecf525e61e..5de2ccf584 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/LayerList.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/LayerList.java @@ -19,6 +19,7 @@ import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jtstest.testbuilder.AppStrings; import org.locationtech.jtstest.testbuilder.geom.ComponentLocater; import org.locationtech.jtstest.testbuilder.geom.GeometryLocation; import org.locationtech.jtstest.testbuilder.geom.SegmentExtracter; @@ -51,9 +52,9 @@ public LayerList() } void initFixed() { - layers.add(new Layer("A")); - layers.add(new Layer("B")); - layers.add(new Layer("Result")); + layers.add(new Layer(AppStrings.GEOM_LABEL_A)); + layers.add(new Layer(AppStrings.GEOM_LABEL_B)); + layers.add(new Layer(AppStrings.GEOM_LABEL_RESULT)); } public int size() { return layers.size(); } From 7d9bf9fac4c30f7f5419cbd99848dce08bd63318 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 4 Dec 2023 14:23:58 -0800 Subject: [PATCH 048/123] Fix TopologyPreservingSimplifier to prevent incorrect topology from jumping components (#1024) --- .../jts/algorithm/RayCrossingCounter.java | 27 ++- .../jts/simplify/ComponentJumpChecker.java | 145 ++++++++++++++++ .../jts/simplify/LineSegmentIndex.java | 8 +- .../jts/simplify/TaggedLineString.java | 39 +++-- .../simplify/TaggedLineStringSimplifier.java | 160 +++++++++++------- .../jts/simplify/TaggedLinesSimplifier.java | 8 +- .../TopologyPreservingSimplifier.java | 34 ++-- .../TopologyPreservingSimplifierTest.java | 28 ++- .../testxml/general/TestSimplify.xml | 28 ++- 9 files changed, 360 insertions(+), 117 deletions(-) create mode 100644 modules/core/src/main/java/org/locationtech/jts/simplify/ComponentJumpChecker.java diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/RayCrossingCounter.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/RayCrossingCounter.java index 3e22a7331d..6f1de5689f 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/RayCrossingCounter.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/RayCrossingCounter.java @@ -174,15 +174,24 @@ public void countSegment(Coordinate p1, Coordinate p2) { } } -/** - * Reports whether the point lies exactly on one of the supplied segments. - * This method may be called at any time as segments are processed. - * If the result of this method is true, - * no further segments need be supplied, since the result - * will never change again. - * - * @return true if the point lies exactly on a segment - */ + /** + * Gets the count of crossings. + * + * @return the crossing count + */ + public int getCount() { + return crossingCount; + } + + /** + * Reports whether the point lies exactly on one of the supplied segments. + * This method may be called at any time as segments are processed. + * If the result of this method is true, + * no further segments need be supplied, since the result + * will never change again. + * + * @return true if the point lies exactly on a segment + */ public boolean isOnSegment() { return isPointOnSegment; } /** diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/ComponentJumpChecker.java b/modules/core/src/main/java/org/locationtech/jts/simplify/ComponentJumpChecker.java new file mode 100644 index 0000000000..286a27f2d9 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/ComponentJumpChecker.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.simplify; + +import java.util.Collection; + +import org.locationtech.jts.algorithm.RayCrossingCounter; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.LineSegment; + +/** + * Checks if simplifying (flattening) line sections or segments + * would cause them to "jump" over other components in the geometry. + * + * @author mdavis + * + */ +class ComponentJumpChecker { + + //TODO: use a spatial index? + private Collection components; + + public ComponentJumpChecker(Collection taggedLines) { + components = taggedLines; + } + + /** + * Checks if a line section jumps a component if flattened. + * + * Assumes start <= end. + * + * @param line the line containing the section being flattened + * @param start start index of the section + * @param end end index of the section + * @param seg the flattening segment + * @return true if the flattened section jumps a component + */ + public boolean hasJump(TaggedLineString line, int start, int end, LineSegment seg) { + Envelope sectionEnv = computeEnvelope(line, start, end); + for (TaggedLineString comp : components) { + //-- don't test component against itself + if (comp == line) + continue; + + Coordinate compPt = comp.getComponentPoint(); + if (sectionEnv.intersects(compPt)) { + if (hasJumpAtComponent(compPt, line, start, end, seg)) { + return true; + } + } + } + return false; + } + + /** + * Checks if two consecutive segments jumps a component if flattened. + * The segments are assumed to be consecutive. + * (so the seg1.p1 = seg2.p0). + * The flattening segment must be the segment between seg1.p0 and seg2.p1. + * + * @param line the line containing the section being flattened + * @param seg1 the first replaced segment + * @param seg2 the next replaced segment + * @param seg the flattening segment + * @return true if the flattened segment jumps a component + */ + public boolean hasJump(TaggedLineString line, LineSegment seg1, LineSegment seg2, LineSegment seg) { + Envelope sectionEnv = computeEnvelope(seg1, seg2); + for (TaggedLineString comp : components) { + //-- don't test component against itself + if (comp == line) + continue; + + Coordinate compPt = comp.getComponentPoint(); + if (sectionEnv.intersects(compPt)) { + if (hasJumpAtComponent(compPt, seg1, seg2, seg)) { + return true; + } + } + } + return false; + } + + private static boolean hasJumpAtComponent(Coordinate compPt, TaggedLineString line, int start, int end, LineSegment seg) { + int sectionCount = crossingCount(compPt, line, start, end); + int segCount = crossingCount(compPt, seg); + boolean hasJump = sectionCount % 2 != segCount % 2; + return hasJump; + } + + private static boolean hasJumpAtComponent(Coordinate compPt, LineSegment seg1, LineSegment seg2, LineSegment seg) { + int sectionCount = crossingCount(compPt, seg1, seg2); + int segCount = crossingCount(compPt, seg); + boolean hasJump = sectionCount % 2 != segCount % 2; + return hasJump; + } + + private static int crossingCount(Coordinate compPt, LineSegment seg) { + RayCrossingCounter rcc = new RayCrossingCounter(compPt); + rcc.countSegment(seg.p0, seg.p1); + return rcc.getCount(); + } + + private static int crossingCount(Coordinate compPt, LineSegment seg1, LineSegment seg2) { + RayCrossingCounter rcc = new RayCrossingCounter(compPt); + rcc.countSegment(seg1.p0, seg1.p1); + rcc.countSegment(seg2.p0, seg2.p1); + return rcc.getCount(); + } + + private static int crossingCount(Coordinate compPt, TaggedLineString line, int start, int end) { + RayCrossingCounter rcc = new RayCrossingCounter(compPt); + for (int i = start; i < end; i++) { + rcc.countSegment(line.getCoordinate(i), line.getCoordinate(i + 1)); + } + return rcc.getCount(); + } + + private static Envelope computeEnvelope(LineSegment seg1, LineSegment seg2) { + Envelope env = new Envelope(); + env.expandToInclude(seg1.p0); + env.expandToInclude(seg1.p1); + env.expandToInclude(seg2.p0); + env.expandToInclude(seg2.p1); + return env; + } + + private static Envelope computeEnvelope(TaggedLineString line, int start, int end) { + Envelope env = new Envelope(); + for (int i = start; i <= end; i++) { + env.expandToInclude(line.getCoordinate(i)); + } + return env; + } +} diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/LineSegmentIndex.java b/modules/core/src/main/java/org/locationtech/jts/simplify/LineSegmentIndex.java index 23cc853ec1..219d3afdc2 100644 --- a/modules/core/src/main/java/org/locationtech/jts/simplify/LineSegmentIndex.java +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/LineSegmentIndex.java @@ -52,13 +52,13 @@ public void remove(LineSegment seg) index.remove(new Envelope(seg.p0, seg.p1), seg); } - public List query(LineSegment querySeg) + public List query(LineSegment querySeg) { Envelope env = new Envelope(querySeg.p0, querySeg.p1); LineSegmentVisitor visitor = new LineSegmentVisitor(querySeg); index.query(env, visitor); - List itemsFound = visitor.getItems(); + List itemsFound = visitor.getItems(); // List listQueryItems = index.query(env); // System.out.println("visitor size = " + itemsFound.size() @@ -78,7 +78,7 @@ class LineSegmentVisitor // MD - only seems to make about a 10% difference in overall time. private LineSegment querySeg; - private ArrayList items = new ArrayList(); + private ArrayList items = new ArrayList(); public LineSegmentVisitor(LineSegment querySeg) { this.querySeg = querySeg; @@ -91,5 +91,5 @@ public void visitItem(Object item) items.add(item); } - public ArrayList getItems() { return items; } + public ArrayList getItems() { return items; } } diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineString.java b/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineString.java index e5b8fd82b0..7530e4404d 100644 --- a/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineString.java +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineString.java @@ -32,23 +32,19 @@ class TaggedLineString private LineString parentLine; private TaggedLineSegment[] segs; - private List resultSegs = new ArrayList(); + private List resultSegs = new ArrayList(); private int minimumSize; - private boolean isPreserveEndpoint = true; + private boolean isRing = true; - public TaggedLineString(LineString parentLine) { - this(parentLine, 2, true); - } - - public TaggedLineString(LineString parentLine, int minimumSize, boolean isPreserveEndpoint) { + public TaggedLineString(LineString parentLine, int minimumSize, boolean isRing) { this.parentLine = parentLine; this.minimumSize = minimumSize; - this.isPreserveEndpoint = isPreserveEndpoint; + this.isRing = isRing; init(); } - public boolean isPreserveEndpoint() { - return isPreserveEndpoint; + public boolean isRing() { + return isRing; } public int getMinimumSize() { return minimumSize; } @@ -56,6 +52,18 @@ public boolean isPreserveEndpoint() { public Coordinate[] getParentCoordinates() { return parentLine.getCoordinates(); } public Coordinate[] getResultCoordinates() { return extractCoordinates(resultSegs); } + public Coordinate getCoordinate(int i) { + return parentLine.getCoordinateN(i); + } + + public int size() { + return parentLine.getNumPoints(); + } + + public Coordinate getComponentPoint() { + return getParentCoordinates()[1]; + } + public int getResultSize() { int resultSegsSize = resultSegs.size(); @@ -91,6 +99,13 @@ private void init() public TaggedLineSegment[] getSegments() { return segs; } + /** + * Add a simplified segment to the result. + * This assumes simplified segments are computed in the order + * they occur in the line. + * + * @param seg the result segment to add + */ public void addToResult(LineSegment seg) { resultSegs.add(seg); @@ -105,7 +120,7 @@ public LinearRing asLinearRing() { return parentLine.getFactory().createLinearRing(extractCoordinates(resultSegs)); } - private static Coordinate[] extractCoordinates(List segs) + private static Coordinate[] extractCoordinates(List segs) { Coordinate[] pts = new Coordinate[segs.size() + 1]; LineSegment seg = null; @@ -126,4 +141,6 @@ void removeRingEndpoint() firstSeg.p0 = lastSeg.p0; resultSegs.remove(resultSegs.size() - 1); } + + } diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineStringSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineStringSimplifier.java index b481d4c173..2101915a8e 100644 --- a/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineStringSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineStringSimplifier.java @@ -16,6 +16,7 @@ import java.util.List; import org.locationtech.jts.algorithm.LineIntersector; +import org.locationtech.jts.algorithm.Orientation; import org.locationtech.jts.algorithm.RobustLineIntersector; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateArrays; @@ -32,28 +33,19 @@ public class TaggedLineStringSimplifier { private LineIntersector li = new RobustLineIntersector(); - private LineSegmentIndex inputIndex = new LineSegmentIndex(); - private LineSegmentIndex outputIndex = new LineSegmentIndex(); + private LineSegmentIndex inputIndex; + private LineSegmentIndex outputIndex; + private ComponentJumpChecker jumpChecker; private TaggedLineString line; private Coordinate[] linePts; - private double distanceTolerance = 0.0; public TaggedLineStringSimplifier(LineSegmentIndex inputIndex, - LineSegmentIndex outputIndex) + LineSegmentIndex outputIndex, + ComponentJumpChecker crossChecker) { this.inputIndex = inputIndex; this.outputIndex = outputIndex; - } - - /** - * Sets the distance tolerance for the simplification. - * All vertices in the simplified geometry will be within this - * distance of the original geometry. - * - * @param distanceTolerance the approximation tolerance to use - */ - public void setDistanceTolerance(double distanceTolerance) { - this.distanceTolerance = distanceTolerance; + this.jumpChecker = crossChecker; } /** @@ -61,25 +53,28 @@ public void setDistanceTolerance(double distanceTolerance) { * using the distance tolerance specified. * * @param line the linestring to simplify + * @param distanceTolerance the simplification distance tolerance */ - void simplify(TaggedLineString line) + void simplify(TaggedLineString line, double distanceTolerance) { this.line = line; linePts = line.getParentCoordinates(); - simplifySection(0, linePts.length - 1, 0); + simplifySection(0, linePts.length - 1, 0, distanceTolerance); - if (! line.isPreserveEndpoint() && CoordinateArrays.isRing(linePts)) { - simplifyRingEndpoint(); + if (line.isRing() && CoordinateArrays.isRing(linePts)) { + simplifyRingEndpoint(distanceTolerance); } } - private void simplifySection(int i, int j, int depth) + private void simplifySection(int i, int j, int depth, double distanceTolerance) { depth += 1; + //-- if section has only one segment just keep the segment if ((i+1) == j) { LineSegment newSeg = line.getSegment(i); line.addToResult(newSeg); - // leave this segment in the input index, for efficiency + //-- do not add segment to output index, since it is unchanged + //-- leave the segment in the input index, for efficiency return; } @@ -99,26 +94,35 @@ private void simplifySection(int i, int j, int depth) double[] distance = new double[1]; int furthestPtIndex = findFurthestPoint(linePts, i, j, distance); + // flattening must be less than distanceTolerance - if (distance[0] > distanceTolerance) isValidToSimplify = false; - // test if flattened section would cause intersection - LineSegment candidateSeg = new LineSegment(); - candidateSeg.p0 = linePts[i]; - candidateSeg.p1 = linePts[j]; - if (hasBadIntersection(line, i, j, candidateSeg)) { + if (distance[0] > distanceTolerance) { isValidToSimplify = false; } - + + if (isValidToSimplify) { + // test if flattened section would cause intersection or jump + LineSegment flatSeg = new LineSegment(); + flatSeg.p0 = linePts[i]; + flatSeg.p1 = linePts[j]; + isValidToSimplify = isTopologyValid(line, i, j, flatSeg); + } + if (isValidToSimplify) { LineSegment newSeg = flatten(i, j); line.addToResult(newSeg); return; } - simplifySection(i, furthestPtIndex, depth); - simplifySection(furthestPtIndex, j, depth); + simplifySection(i, furthestPtIndex, depth, distanceTolerance); + simplifySection(furthestPtIndex, j, depth, distanceTolerance); } - private void simplifyRingEndpoint() + /** + * Simplifies the result segments on either side of a ring endpoint + * (which was not processed by the initial simplification). + * This ensures that simplification removes flat (collinear) endpoints. + */ + private void simplifyRingEndpoint(double distanceTolerance) { if (line.getResultSize() > line.getMinimumSize()) { LineSegment firstSeg = line.getResultSegment(0); @@ -126,13 +130,14 @@ private void simplifyRingEndpoint() LineSegment simpSeg = new LineSegment(lastSeg.p0, firstSeg.p1); //-- the excluded segments are the ones containing the endpoint - if (simpSeg.distance(firstSeg.p0) <= distanceTolerance - && ! hasBadIntersection(line, line.getSegments().length - 2, 0, simpSeg)) { + Coordinate endPt = firstSeg.p0; + if (simpSeg.distance(endPt) <= distanceTolerance + && isTopologyValid(line, firstSeg, lastSeg, simpSeg)) { line.removeRingEndpoint(); } } } - + private int findFurthestPoint(Coordinate[] pts, int i, int j, double[] maxDistance) { LineSegment seg = new LineSegment(); @@ -169,61 +174,90 @@ private LineSegment flatten(int start, int end) Coordinate p0 = linePts[start]; Coordinate p1 = linePts[end]; LineSegment newSeg = new LineSegment(p0, p1); - // update the indexes - remove(line, start, end); + // update the input and output indexes outputIndex.add(newSeg); + remove(line, start, end); + return newSeg; } /** - * Tests if a flattening segment intersects a line - * (excluding a given section of segments). - * The excluded section is being replaced by the flattening segment, + * Tests if line topology remains valid after flattening a section of the line. + * The flattened section is being replaced by the flattening segment, * so there is no need to test it * (and it may well intersect the segment). * * @param line - * @param excludeStart - * @param excludeEnd - * @param candidateSeg - * @return + * @param sectionStart + * @param sectionEnd + * @param flatSeg + * @return true if the flattening leaves valid topology */ - private boolean hasBadIntersection(TaggedLineString line, - int excludeStart, int excludeEnd, - LineSegment candidateSeg) + private boolean isTopologyValid(TaggedLineString line, + int sectionStart, int sectionEnd, + LineSegment flatSeg) { - if (hasBadOutputIntersection(candidateSeg)) return true; - if (hasBadInputIntersection(line, excludeStart, excludeEnd, candidateSeg)) return true; - return false; + if (hasOutputIntersection(flatSeg)) + return false; + if (hasInputIntersection(line, sectionStart, sectionEnd, flatSeg)) + return false; + if (jumpChecker.hasJump(line, sectionStart, sectionEnd, flatSeg)) + return false; + return true; } - private boolean hasBadOutputIntersection(LineSegment candidateSeg) + private boolean isTopologyValid(TaggedLineString line, LineSegment seg1, LineSegment seg2, + LineSegment flatSeg) { + //-- if segments are already flat, topology is unchanged and so is valid + //-- (otherwise, output and/or input intersection test would report false positive) + if (isCollinear(seg1.p0, flatSeg)) + return true; + if (hasOutputIntersection(flatSeg)) + return false; + if (hasInputIntersection(flatSeg)) + return false; + if (jumpChecker.hasJump(line, seg1, seg2, flatSeg)) + return false; + return true; + } + + private boolean isCollinear(Coordinate pt, LineSegment seg) { + return Orientation.COLLINEAR == seg.orientationIndex(pt); + } + + private boolean hasOutputIntersection(LineSegment flatSeg) { - List querySegs = outputIndex.query(candidateSeg); + List querySegs = outputIndex.query(flatSeg); for (Iterator i = querySegs.iterator(); i.hasNext(); ) { LineSegment querySeg = (LineSegment) i.next(); - if (hasInvalidIntersection(querySeg, candidateSeg)) { + if (hasInvalidIntersection(querySeg, flatSeg)) { return true; } } return false; } - private boolean hasBadInputIntersection(TaggedLineString line, + private boolean hasInputIntersection(LineSegment flatSeg) + { + return hasInputIntersection(null, -1, -1, flatSeg); + } + + private boolean hasInputIntersection(TaggedLineString line, int excludeStart, int excludeEnd, - LineSegment candidateSeg) + LineSegment flatSeg) { - List querySegs = inputIndex.query(candidateSeg); + List querySegs = inputIndex.query(flatSeg); for (Iterator i = querySegs.iterator(); i.hasNext(); ) { TaggedLineSegment querySeg = (TaggedLineSegment) i.next(); - if (hasInvalidIntersection(querySeg, candidateSeg)) { - /** - * Ignore the intersection if the intersecting segment is part of the section being collapsed - * to the candidate segment - */ - if (isInLineSection(line, excludeStart, excludeEnd, querySeg)) - continue; - return true; + if (hasInvalidIntersection(querySeg, flatSeg)) { + /** + * Ignore the intersection if the intersecting segment is part of the section being collapsed + * to the candidate segment + */ + if (line != null + && isInLineSection(line, excludeStart, excludeEnd, querySeg)) + continue; + return true; } } return false; diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLinesSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLinesSimplifier.java index e14aa7404f..d3642ac082 100644 --- a/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLinesSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLinesSimplifier.java @@ -25,6 +25,7 @@ class TaggedLinesSimplifier { private LineSegmentIndex inputIndex = new LineSegmentIndex(); private LineSegmentIndex outputIndex = new LineSegmentIndex(); + private double distanceTolerance = 0.0; public TaggedLinesSimplifier() @@ -49,14 +50,15 @@ public void setDistanceTolerance(double distanceTolerance) { * @param taggedLines the collection of lines to simplify */ public void simplify(Collection taggedLines) { + ComponentJumpChecker jumpChecker = new ComponentJumpChecker(taggedLines); + for (Iterator i = taggedLines.iterator(); i.hasNext(); ) { inputIndex.add((TaggedLineString) i.next()); } for (Iterator i = taggedLines.iterator(); i.hasNext(); ) { TaggedLineStringSimplifier tlss - = new TaggedLineStringSimplifier(inputIndex, outputIndex); - tlss.setDistanceTolerance(distanceTolerance); - tlss.simplify((TaggedLineString) i.next()); + = new TaggedLineStringSimplifier(inputIndex, outputIndex, jumpChecker); + tlss.simplify((TaggedLineString) i.next(), distanceTolerance); } } diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/TopologyPreservingSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/simplify/TopologyPreservingSimplifier.java index eb154aafb2..5eb8ab58e2 100644 --- a/modules/core/src/main/java/org/locationtech/jts/simplify/TopologyPreservingSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/TopologyPreservingSimplifier.java @@ -28,8 +28,7 @@ * Simplifies a geometry and ensures that * the result is a valid geometry having the * same dimension and number of components as the input, - * and with the components having the same topological - * relationship. + * and with the components having the same topological relationship. *

* If the input is a polygonal geometry * ( {@link Polygon} or {@link MultiPolygon} ): @@ -45,8 +44,8 @@ * any intersecting line segments, this property * will be preserved in the output. *

- * For polygonal geometries and LinearRings the endpoint will participate - * in simplification. For LineStrings the endpoints will not be unchanged. + * For polygonal geometries and LinearRings the ring endpoint will be simplified. + * For LineStrings the endpoints will be unchanged. *

* For all geometry types, the result will contain * enough vertices to ensure validity. For polygons @@ -60,19 +59,6 @@ *

* The simplification uses a maximum-distance difference algorithm * similar to the Douglas-Peucker algorithm. - * - *

KNOWN BUGS

- *
    - *
  • May create invalid topology if there are components which are - * small relative to the tolerance value. - * In particular, if a small hole is very near an edge, it is possible for the edge to be moved by - * a relatively large tolerance value and end up with the hole outside the result shell - * (or inside another hole). - * Similarly, it is possible for a small polygon component to end up inside - * a nearby larger polygon. - * A workaround is to test for this situation in post-processing and remove - * any invalid holes or polygons. - *
* * @author Martin Davis * @see DouglasPeuckerSimplifier @@ -89,7 +75,7 @@ public static Geometry simplify(Geometry geom, double distanceTolerance) private Geometry inputGeom; private TaggedLinesSimplifier lineSimplifier = new TaggedLinesSimplifier(); - private Map linestringMap; + private Map linestringMap; public TopologyPreservingSimplifier(Geometry inputGeom) { @@ -116,7 +102,7 @@ public Geometry getResultGeometry() // empty input produces an empty result if (inputGeom.isEmpty()) return inputGeom.copy(); - linestringMap = new HashMap(); + linestringMap = new HashMap(); inputGeom.apply(new LineStringMapBuilderFilter(this)); lineSimplifier.simplify(linestringMap.values()); Geometry result = (new LineStringTransformer(linestringMap)).transform(inputGeom); @@ -126,9 +112,9 @@ public Geometry getResultGeometry() static class LineStringTransformer extends GeometryTransformer { - private Map linestringMap; + private Map linestringMap; - public LineStringTransformer(Map linestringMap) { + public LineStringTransformer(Map linestringMap) { this.linestringMap = linestringMap; } @@ -137,7 +123,7 @@ protected CoordinateSequence transformCoordinates(CoordinateSequence coords, Geo if (coords.size() == 0) return null; // for linear components (including rings), simplify the linestring if (parent instanceof LineString) { - TaggedLineString taggedLine = (TaggedLineString) linestringMap.get(parent); + TaggedLineString taggedLine = linestringMap.get(parent); return createCoordinateSequence(taggedLine.getResultCoordinates()); } // for anything else (e.g. points) just copy the coordinates @@ -178,8 +164,8 @@ public void filter(Geometry geom) if (line.isEmpty()) return; int minSize = ((LineString) line).isClosed() ? 4 : 2; - boolean isPreserveEndpoint = (line instanceof LinearRing) ? false : true; - TaggedLineString taggedLine = new TaggedLineString((LineString) line, minSize, isPreserveEndpoint); + boolean isRing = (line instanceof LinearRing) ? true : false; + TaggedLineString taggedLine = new TaggedLineString((LineString) line, minSize, isRing); tps.linestringMap.put(line, taggedLine); } } diff --git a/modules/core/src/test/java/org/locationtech/jts/simplify/TopologyPreservingSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/simplify/TopologyPreservingSimplifierTest.java index e30cc26ce1..b8a2187791 100644 --- a/modules/core/src/test/java/org/locationtech/jts/simplify/TopologyPreservingSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/simplify/TopologyPreservingSimplifierTest.java @@ -180,6 +180,14 @@ public void testPolygonRemoveFlatEndpoint() throws Exception { ); } + public void testPolygonManyFlatSegments() throws Exception { + checkTPS( + "POLYGON ((5 5, 7 5, 9 5, 9 1, 1 1, 1 5, 3 5, 5 5))", + 1, + "POLYGON ((9 5, 9 1, 1 1, 1 5, 9 5))" + ); + } + //-- vertex is not removed due to overly-restrictive heuristic result length calculation? public void testPolygonSize5NotSimplfied() throws Exception { checkTPS( @@ -199,7 +207,7 @@ public void testPolygonSize5NotSimplfied() throws Exception { public void testMultiPolygonWithSmallComponents() throws Exception { checkTPS("MULTIPOLYGON(((13.73095 51.024734,13.7309323 51.0247668,13.7306959 51.0247959,13.7292724 51.0249742,13.7280216 51.0251252,13.7266598 51.0252998,13.7259617 51.0254072,13.7258854 51.0254201,13.7253253 51.0255144,13.725276 51.025492,13.724538 51.025631,13.7230288 51.0259021,13.7223529 51.0260273,13.7223299 51.0260863,13.7222292 51.026391,13.7220002 51.0273366,13.7217875 51.0282094,13.721746 51.028243,13.7217693 51.0282803,13.7215512 51.0291967,13.721513 51.029222,13.7215203 51.0292567,13.7212713 51.0295967,13.7222258 51.0299532,13.722234 51.03,13.7222931 51.0299823,13.7232514 51.0303187,13.7242514 51.0306715,13.724263 51.030714,13.7243024 51.0306951,13.7249934 51.0309315,13.7265097 51.0314552,13.7266116 51.0313952,13.7267988 51.0313334,13.7269952 51.0313243,13.72703 51.0314107,13.7271637 51.0313254,13.7272524 51.0313839,13.72739 51.031449,13.7276768 51.0313074,13.7283793 51.0309944,13.7296654 51.0304157,13.7297572 51.0303637,13.729845 51.0303139,13.7299557 51.0301763,13.7300964 51.0300176,13.730252 51.0298919,13.7304615 51.0297932,13.730668 51.0297363,13.730743 51.029783,13.7307859 51.0298398,13.7307094 51.0301388,13.730624 51.030263,13.7306955 51.0303267,13.7301182 51.0325594,13.7300528 51.0325663,13.7301114 51.0327342,13.7301645 51.0329094,13.7300035 51.0327693,13.7299669 51.0327351,13.7299445 51.0327211,13.7298934 51.032814,13.7298539 51.0328585,13.7297737 51.0328321,13.7288526 51.0325639,13.7288201 51.0324367,13.7284426 51.0324383,13.7276461 51.032179,13.7274569 51.0321976,13.7272787 51.0322421,13.7271265 51.0322903,13.7267034 51.0322495,13.7265364 51.0322161,13.7259018 51.0324269,13.7258649 51.03242,13.725733 51.0326646,13.7251933 51.0328876,13.7247918 51.0331374,13.7244439 51.0331106,13.7242967 51.0334273,13.7239131 51.0337529,13.7237035 51.0338511,13.7235429 51.033967,13.7233375 51.0339148,13.7232064 51.0339347,13.7231786 51.0339863,13.7228848 51.0340776,13.7224481 51.0341888,13.7220471 51.0342483,13.7217493 51.0343198,13.721552 51.0343861,13.7214718 51.0344095,13.7215108 51.034534,13.7205032 51.0349932,13.7197657 51.0352983,13.7195764 51.0352291,13.7195934 51.0352797,13.7182451 51.0359157,13.7181108 51.0359003,13.7181657 51.0359571,13.717622 51.0361956,13.7159749 51.0369683,13.7159057 51.0369284,13.7158604 51.0370288,13.7157161 51.0370124,13.7157523 51.0370733,13.7153708 51.0372801,13.7150274 51.0374899,13.7144074 51.0379192,13.7138287 51.0383899,13.7137514 51.0383857,13.7137492 51.0384566,13.7134249 51.0387269,13.7130179 51.0390385,13.7125791 51.0393343,13.7120736 51.039611,13.7115839 51.0398558,13.7112945 51.0399894,13.7114637 51.0402313,13.7123153 51.041449,13.7126333 51.0417033,13.713371 51.0421453,13.7138861 51.0424061,13.7142518 51.0425683,13.7164587 51.0435668,13.7167995 51.0437957,13.7170883 51.0439897,13.7190694 51.0451663,13.7196131 51.0458277,13.7197562 51.0461521,13.7198262 51.0464192,13.7198377 51.0467389,13.7205681 51.0455573,13.7210009 51.0450379,13.7214987 51.0445401,13.7220306 51.0442859,13.7227215 51.0439558,13.7237962 51.0434514,13.723979 51.0435278,13.7241448 51.0435041,13.7241052 51.0436042,13.7247987 51.0438896,13.7250186 51.0439093,13.7250579 51.0440386,13.7257225 51.0443545,13.7259312 51.0443456,13.725955 51.0443813,13.7260235 51.0443873,13.7260682 51.0445303,13.7282191 51.0455848,13.7290532 51.045927,13.7292643 51.0458591,13.7292228 51.0459969,13.729706 51.0461854,13.7303185 51.046393,13.7309107 51.0465601,13.731546 51.0466841,13.7321939 51.0467752,13.7332896 51.0468999,13.7333733 51.0469094,13.7334778 51.0468127,13.7335706 51.0469078,13.733651 51.0470684,13.7338458 51.0471508,13.7346109 51.0472333,13.7346367 51.0471474,13.7346922 51.0470697,13.7346666 51.0470056,13.7346564 51.0468714,13.7345552 51.0467095,13.7336001 51.0465496,13.733427 51.046454,13.7335317 51.0464255,13.7347225 51.0465948,13.7348421 51.0466562,13.7349123 51.0466203,13.736811 51.0468537,13.7382043 51.0469796,13.7383487 51.0469803,13.7394909 51.0469005,13.7400899 51.0467949,13.7405051 51.0464739,13.7408331 51.0462204,13.7412027 51.0463256,13.741053 51.0466451,13.7407291 51.0469007,13.7405095 51.0469726,13.7400888 51.0470337,13.7393051 51.0471049,13.7393014 51.0472015,13.7393088 51.0473019,13.7395556 51.0473056,13.7404944 51.0472245,13.740932 51.0470192,13.7414421 51.0465652,13.7414893 51.0465576,13.7416494 51.0464916,13.7416003 51.0466074,13.7416246 51.04663,13.741668 51.0466443,13.7417272 51.0467159,13.7417503 51.0466716,13.7423587 51.0468732,13.7426958 51.0470246,13.7429143 51.0471813,13.74318 51.04726,13.7430363 51.0472995,13.7433021 51.047588,13.7434678 51.0475916,13.7433805 51.0477019,13.7436362 51.0479981,13.7446308 51.0491622,13.7447961 51.0491827,13.744722 51.0492509,13.7448536 51.0494078,13.745056 51.0494766,13.7450313 51.0496901,13.7453573 51.0500052,13.7465317 51.0512807,13.7466999 51.0513722,13.746638 51.0514149,13.7468683 51.0516781,13.7470071 51.051777,13.7469985 51.0518746,13.7470732 51.0519866,13.7471316 51.0520528,13.7472989 51.0523089,13.7472368 51.0523858,13.7473063 51.0524932,13.7473468 51.0527412,13.7473392 51.0531614,13.7472987 51.0533157,13.7473919 51.0534224,13.7472684 51.0534549,13.7472134 51.0536926,13.7472913 51.0537784,13.7473216 51.053725,13.7474649 51.0537575,13.7474492 51.053833,13.7475625 51.0537839,13.7497379 51.0544435,13.7515333 51.0551019,13.7527693 51.0555438,13.7549766 51.0564993,13.7550622 51.0565364,13.755105 51.0566612,13.7552745 51.0566237,13.7558661 51.0560648,13.7559318 51.0560101,13.755908 51.055897,13.7559252 51.0558292,13.7559566 51.0557055,13.7564494 51.0551377,13.7564124 51.0550457,13.7573213 51.0539813,13.7575007 51.0539933,13.757856 51.0540047,13.7580394 51.054028,13.7580896 51.053984,13.7580949 51.0539463,13.7579963 51.0538534,13.7581294 51.0537147,13.7582346 51.0535957,13.758354 51.053433,13.758363 51.053392,13.7583656 51.0533457,13.758359 51.0532095,13.7583338 51.0530937,13.7582902 51.0529647,13.7580365 51.0522637,13.7577683 51.051463,13.7573182 51.0501993,13.7571595 51.0497164,13.7567579 51.0490095,13.7563383 51.0482979,13.7557757 51.0473383,13.7557095 51.0472522,13.7555771 51.0471199,13.7554448 51.0470471,13.7548596 51.0462612,13.7547097 51.046054,13.7549127 51.0460086,13.7548633 51.0459174,13.7548127 51.0458413,13.7547176 51.0457237,13.7538293 51.0449222,13.7530218 51.0441346,13.7526711 51.0437838,13.752446 51.0435522,13.7522297 51.0433547,13.751704 51.042833,13.7513058 51.0424448,13.7505766 51.0417281,13.7499967 51.0411283,13.7497695 51.0408943,13.7493849 51.0405205,13.7486222 51.0397896,13.7478209 51.0390261,13.7477474 51.0389532,13.7477041 51.0389189,13.7476277 51.0388729,13.7475781 51.0388513,13.7472699 51.038726,13.747131 51.0386506,13.7469329 51.0385052,13.7468562 51.0384284,13.7466683 51.0383483,13.7467998 51.038236,13.7473841 51.0380129,13.747838 51.0378277,13.7481801 51.0376558,13.7489728 51.0370285,13.7491313 51.0368016,13.7492665 51.0363477,13.7493166 51.0359389,13.7492966 51.0358087,13.7493888 51.0356942,13.7492867 51.0357016,13.7492855 51.0354359,13.7492829 51.034867,13.7492723 51.0348311,13.7492455 51.0347398,13.7493034 51.0346612,13.7491987 51.0346142,13.748866 51.034723,13.748791 51.034201,13.748335 51.034159,13.748294 51.034034,13.748205 51.033764,13.7488691 51.0333037,13.748962 51.033245,13.7486777 51.0332252,13.7483008 51.032683,13.7484397 51.0324582,13.7469913 51.0327817,13.7466998 51.0326205,13.7459997 51.0314852,13.7460996 51.0313569,13.745967 51.0314864,13.7449355 51.0317377,13.7447301 51.0316513,13.7446705 51.0318463,13.7420262 51.0323659,13.7419131 51.0322884,13.7418636 51.0322552,13.7416501 51.0321425,13.7415567 51.0317708,13.7414972 51.0314666,13.741484 51.0311492,13.741923 51.031003,13.7418649 51.030884,13.74209 51.0304134,13.7422077 51.0300143,13.7421975 51.0299222,13.742286 51.029835,13.7421463 51.0297533,13.7420951 51.0296254,13.7415933 51.0288452,13.7414906 51.0286855,13.7414437 51.0286127,13.7413482 51.0284642,13.7410545 51.0280777,13.7407158 51.0277229,13.7401513 51.0273842,13.7392803 51.0270293,13.7382744 51.0267844,13.737321 51.0267454,13.7365929 51.0267541,13.736556 51.026812,13.7364715 51.026754,13.7357088 51.0268017,13.7353967 51.02678,13.73534 51.02685,13.7352667 51.0267757,13.734907 51.0267324,13.734824 51.02679,13.7347684 51.0267064,13.7342093 51.0266674,13.73409 51.026725,13.7340359 51.0266283,13.7335072 51.0265633,13.733407 51.02663,13.7333208 51.0265373,13.7317087 51.0263813,13.7317173 51.0263119,13.73167 51.026241,13.7317563 51.0261602,13.7318473 51.0258395,13.7318647 51.0254971,13.73183 51.0253281,13.7317736 51.0252414,13.731663 51.025181,13.7316826 51.0251114,13.7310803 51.0247604,13.73095 51.024734)),((13.7368533 51.0470386,13.7368426 51.0471226,13.7368067 51.0472669,13.7368255 51.0473828,13.7369099 51.0474154,13.7376695 51.0474677,13.7382756 51.0474245,13.738513 51.0474297,13.7386105 51.0474065,13.738705 51.0473737,13.7385856 51.0473757,13.7385618 51.0473751,13.7385263 51.0473743,13.7384706 51.0473744,13.7383071 51.0473734,13.7383822 51.0473564,13.7390821 51.047287,13.7390933 51.047209,13.7390933 51.0471421,13.7368533 51.0470386)),((13.7367293 51.0470057,13.7346615 51.0466892,13.7347551 51.0468411,13.7347754 51.0470359,13.7347106 51.0471899,13.7356421 51.0472919,13.7366963 51.0474074,13.736705 51.047249,13.7367293 51.0470057)))", 0.0057, - "MULTIPOLYGON (((13.73095 51.024734, 13.7123153 51.041449, 13.7552745 51.0566237, 13.7484397 51.0324582, 13.73095 51.024734)), ((13.7390933 51.0471421, 13.7369099 51.0474154, 13.7390933 51.047209, 13.7390933 51.0471421)), ((13.7367293 51.0470057, 13.7346615 51.0466892, 13.7347106 51.0471899, 13.7367293 51.0470057)))"); + "MULTIPOLYGON (((13.73095 51.024734, 13.7123153 51.041449, 13.7412027 51.0463256, 13.7552745 51.0566237, 13.7484397 51.0324582, 13.73095 51.024734)), ((13.7390933 51.0471421, 13.7369099 51.0474154, 13.7390933 51.047209, 13.7390933 51.0471421)), ((13.7367293 51.0470057, 13.7346615 51.0466892, 13.7347106 51.0471899, 13.7367293 51.0470057)))"); } /** @@ -212,6 +220,24 @@ public void testPolygonWithSpike() throws Exception { "POLYGON ((3312459.605 6646878.353, 3312460.524 6646875.969, 3312459.427 6646878.421, 3312460.014 6646886.391, 3312465.889 6646887.398, 3312470.827 6646884.839, 3312477.289 6646871.694, 3312472.748 6646869.547, 3312459.605 6646878.353))"); } + public void testLineComponentCross() { + checkTPS("MULTILINESTRING ((0 0, 10 2, 20 0), (9 1, 11 1))", + 4, + "MULTILINESTRING ((0 0, 10 2, 20 0), (9 1, 11 1))"); + } + + public void testPolygonComponentCrossAtEndpoint() { + checkTPS("MULTIPOLYGON (((50 40, 40 60, 80 40, 0 0, 30 70, 50 40)), ((40 56, 40 57, 41 56, 40 56)))", + 30, + "MULTIPOLYGON (((50 40, 80 40, 0 0, 30 70, 50 40)), ((40 56, 40 57, 41 56, 40 56)))"); + } + + public void testPolygonIntersectingSegments() { + checkTPS("MULTIPOLYGON (((0.63 0.2, 0.35 0, 0.73 0.66, 0.63 0.2)), ((1.42 4.01, 3.45 0.7, 1.79 1.47, 0 0.57, 1.42 4.01)))", + 10, + "MULTIPOLYGON (((0.63 0.2, 0.35 0, 0.73 0.66, 0.63 0.2)), ((1.42 4.01, 3.45 0.7, 1.79 1.47, 0 0.57, 1.42 4.01)))"); + } + private void checkTPS(String wkt, double tolerance, String wktExpected) { Geometry geom = read(wkt); Geometry actual = TopologyPreservingSimplifier.simplify(geom, tolerance); diff --git a/modules/tests/src/test/resources/testxml/general/TestSimplify.xml b/modules/tests/src/test/resources/testxml/general/TestSimplify.xml index 72e611de03..d00cc22e1e 100644 --- a/modules/tests/src/test/resources/testxml/general/TestSimplify.xml +++ b/modules/tests/src/test/resources/testxml/general/TestSimplify.xml @@ -62,6 +62,18 @@ + + mL - lines with constrained topology + MULTILINESTRING ((10 60, 39 50, 70 60, 90 50), (35 55, 46 55), (65 55, 75 55), (10 40, 40 30, 70 40, 90 30)) + + + MULTILINESTRING ((10 60, 90 50), (35 55, 46 55), (65 55, 75 55), (10 40, 90 30)) + + + MULTILINESTRING ((10 60, 39 50, 70 60, 90 50), (35 55, 46 55), (65 55, 75 55), (10 40, 90 30)) + + + mL - lines with EMPTY MULTILINESTRING(EMPTY, (10 10, 20 21, 30 30), (10 10, 10 30, 30 30)) @@ -86,6 +98,18 @@ + + A - polygon with multiple flat segments around endpoint + POLYGON ((5 5, 7 5, 9 5, 9 1, 1 1, 1 5, 3 5, 5 5)) + + + POLYGON ((9 5, 9 1, 1 1, 1 5, 9 5)) + + + POLYGON ((9 5, 9 1, 1 1, 1 5, 9 5)) + + + A - polygon simplification POLYGON ((10 10, 10 90, 60.5 87, 90 90, 90 10, 12 12, 10 10)) @@ -155,7 +179,7 @@ A - polygon with small hole near simplified edge DP: hole is remmoved - TP: hole is skipped over - BUG! + TP: hole is preserved POLYGON ((10 10, 10 80, 50 90, 90 80, 90 10, 10 10), (70 81, 30 81, 50 90, 70 81)) @@ -163,7 +187,7 @@ POLYGON ((10 10, 10 80, 90 80, 90 10, 10 10)) - POLYGON ((10 10, 10 80, 90 80, 90 10, 10 10), (70 81, 30 81, 50 90, 70 81)) + POLYGON ((10 10, 10 80, 50 90, 90 80, 90 10, 10 10), (70 81, 30 81, 50 90, 70 81)) From ebf4f11f8f705466c595f61d427c6d0195bbeda1 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 4 Dec 2023 14:24:36 -0800 Subject: [PATCH 049/123] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index ab6a54950a..bddc1b9cf7 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -67,6 +67,7 @@ Distributions for older JTS versions can be obtained at the * Fix `DistanceOp` for empty elements (#1010) * Fix predicates for MultiPoint with EMPTY (#1015) * Fix `InteriorPoint` for MultiLineString with EMPTY (#1023) +* Fix TopologyPreservingSimplifier to prevent incorrect topology from jumping components (#1024) ### Performance Improvements From d26cef981decaf12f10e4971106f2525ee9a66ce Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 6 Dec 2023 11:35:14 -0800 Subject: [PATCH 050/123] Clean up TestBuilder color constants --- .../jtstest/testbuilder/AppColors.java | 26 ++++++- .../jtstest/testbuilder/AppConstants.java | 8 +- .../testbuilder/GeometryDepiction.java | 74 ------------------- .../testbuilder/GeometryEditPanel.java | 4 +- .../testbuilder/GeometryInputDialog.java | 8 +- .../jtstest/testbuilder/InspectorPanel.java | 2 +- .../testbuilder/JTSTestBuilderFrame.java | 4 - .../jtstest/testbuilder/RelatePanel.java | 24 +++--- .../jtstest/testbuilder/ValidPanel.java | 5 +- .../jtstest/testbuilder/WKTPanel.java | 6 +- .../testbuilder/model/TestBuilderModel.java | 14 ++-- 11 files changed, 58 insertions(+), 117 deletions(-) delete mode 100644 modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryDepiction.java diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppColors.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppColors.java index 10265f3e60..f974683738 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppColors.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppColors.java @@ -16,13 +16,31 @@ public class AppColors { - public static final Color A = Color.BLUE; - public static final Color B = Color.RED; - public static final Color Result = Color.GREEN; + public static final Color GEOM_A = Color.BLUE; + public static final Color GEOM_B = Color.RED; + public static final Color GEOM_RESULT = new Color(100, 150, 0); // YellowGreen + public static final Color GEOM_A_HIGHLIGHT_CLR = new Color(0, 0, 255); + public static final Color GEOM_A_LINE_CLR = new Color(0, 0, 255, 150); + public static final Color GEOM_A_FILL_CLR = new Color(200, 200, 255, 150); + public static final Color GEOM_A_BAND = Color.cyan; + + public static final Color GEOM_B_HIGHLIGHT_CLR = new Color(255, 0, 0); + public static final Color GEOM_B_LINE_CLR = new Color(150, 0, 0, 150); + public static final Color GEOM_B_FILL_CLR = new Color(255, 200, 200, 150); + public static final Color GEOM_B_BAND = Color.pink; + // YellowGreen + public static final Color GEOM_RESULT_LINE_CLR = new Color(120, 180, 0, 200); + // Yellow + public static final Color GEOM_RESULT_FILL_CLR = new Color(255, 255, 100, 100); + + public static final Color GEOM_VIEW_BACKGROUND = Color.white; + + public static final Color BACKGROUND_FOCUS = Color.white; public static final Color BACKGROUND = UIManager.getColor ( "Panel.background" ); public static final Color BACKGROUND_ERROR = Color.PINK; public static final Color TAB_FOCUS = UIManager.getColor ("TabbedPane.highlight" ); - public static final Color GEOM_VIEW_BACKGROUND = Color.white; + + } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppConstants.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppConstants.java index b927e7c223..8276105023 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppConstants.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppConstants.java @@ -23,12 +23,12 @@ public class AppConstants { public static final int POINT_SIZE = 5; public static final int VERTEX_SIZE = 4; - public static double HIGHLIGHT_SIZE = 50.0; - public static double VERTEX_SHADOW_SIZE = 100; + public static final double HIGHLIGHT_SIZE = 50.0; + public static final double VERTEX_SHADOW_SIZE = 100; - public static double TOPO_STRETCH_VIEW_DIST = 5; + public static final double TOPO_STRETCH_VIEW_DIST = 5; - public static double MASK_WIDTH_FRAC = 0.3333; + public static final double MASK_WIDTH_FRAC = 0.3333; // a very light gray public static final Color MASK_CLR = new Color(230, 230, 230); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryDepiction.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryDepiction.java deleted file mode 100644 index e98a35915d..0000000000 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryDepiction.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2016 Vivid Solutions. - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 2.0 - * and Eclipse Distribution License v. 1.0 which accompanies this distribution. - * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html - * and the Eclipse Distribution License is available at - * - * http://www.eclipse.org/org/documents/edl-v10.php. - */ -package org.locationtech.jtstest.testbuilder; - -import java.awt.Color; - - -/** - * @version 1.7 - */ -public class GeometryDepiction -{ - - public static final Color GEOM_A_HIGHLIGHT_CLR = new Color(0, 0, 255); - public static final Color GEOM_A_LINE_CLR = new Color(0, 0, 255, 150); - public static final Color GEOM_A_FILL_CLR = new Color(200, 200, 255, 150); - - public static final Color GEOM_B_HIGHLIGHT_CLR = new Color(255, 0, 0); - public static final Color GEOM_B_LINE_CLR = new Color(150, 0, 0, 150); - public static final Color GEOM_B_FILL_CLR = new Color(255, 200, 200, 150); - - // YellowGreen - public static final Color GEOM_RESULT_LINE_CLR = new Color(120, 180, 0, 200); - // Yellow - public static final Color GEOM_RESULT_FILL_CLR = new Color(255, 255, 100, 100); - - public static final GeometryDepiction RESULT = new GeometryDepiction( - new Color(154, 205, 0, 150), - new Color(255, 255, 100, 100), - // Yellow - null); - - public static final GeometryDepiction GEOM_A = new GeometryDepiction( - new Color(0, 0, 255, 150), - new Color(200, 200, 255, 150), - Color.cyan); - - public static final GeometryDepiction GEOM_B = new GeometryDepiction( - new Color(255, 0, 0, 150), - new Color(255, 200, 200, 150), - Color.pink); - - - private Color color; - private Color fillColor; - private Color bandColor; - - public Color getColor() { - return color; - } - - public Color getFillColor() { - return fillColor; - } - - public Color getBandColor() { - return bandColor; - } - - public GeometryDepiction(Color color, Color fillColor, Color bandColor) { - this.color = color; - this.fillColor = fillColor; - this.bandColor = bandColor; - } -} diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryEditPanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryEditPanel.java index 3ca39506c5..4877583f14 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryEditPanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryEditPanel.java @@ -733,8 +733,8 @@ public void renderMagnifiedVertices(Graphics2D g) for (int j = 0; j < stretchedVerts.size(); j++) { Coordinate p = (Coordinate) stretchedVerts.get(j); drawHighlightedVertex(g, p, - i == 0 ? GeometryDepiction.GEOM_A_HIGHLIGHT_CLR : - GeometryDepiction.GEOM_B_HIGHLIGHT_CLR); + i == 0 ? AppColors.GEOM_A_HIGHLIGHT_CLR : + AppColors.GEOM_B_HIGHLIGHT_CLR); } } } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryInputDialog.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryInputDialog.java index bc8ba79800..1a403a67e1 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryInputDialog.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryInputDialog.java @@ -80,7 +80,7 @@ void jbInit() throws Exception { border1 = BorderFactory.createLineBorder(Color.gray, 2); panel1.setLayout(borderLayout1); jLabel1.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel1.setForeground(AppColors.A); + jLabel1.setForeground(AppColors.GEOM_A); jLabel1.setToolTipText(""); jLabel1.setText(AppStrings.GEOM_LABEL_A); jPanel1.setLayout(gridBagLayout2); @@ -100,7 +100,7 @@ public void actionPerformed(ActionEvent e) { } }); jLabel2.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel2.setForeground(AppColors.B); + jLabel2.setForeground(AppColors.GEOM_B); jLabel2.setText(AppStrings.GEOM_LABEL_B); lblError.setToolTipText(""); txtError.setLineWrap(true); @@ -211,9 +211,9 @@ void btnCancel_actionPerformed(ActionEvent e) { void btnLoad_actionPerformed(ActionEvent e) { parseError = false; - geom[0] = parseGeometry(txtA, AppColors.A); + geom[0] = parseGeometry(txtA, AppColors.GEOM_A); if (!parseError) - geom[1] = parseGeometry(txtB, AppColors.B); + geom[1] = parseGeometry(txtB, AppColors.GEOM_B); if (!parseError) setVisible(false); } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/InspectorPanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/InspectorPanel.java index 1504770125..f008b80707 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/InspectorPanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/InspectorPanel.java @@ -199,7 +199,7 @@ public void setGeometry(String tag, Geometry geom, int source, boolean isEditabl btnDelete.setEnabled(isEditable); lblGeom.setText(tag); lblGeom.setToolTipText(tag); - lblGeom.setForeground(source == 0 ? AppColors.A : AppColors.B); + lblGeom.setForeground(source == 0 ? AppColors.GEOM_A : AppColors.GEOM_B); sortNone(); } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/JTSTestBuilderFrame.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/JTSTestBuilderFrame.java index 7e4a44e9c3..750ec09fcb 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/JTSTestBuilderFrame.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/JTSTestBuilderFrame.java @@ -466,10 +466,6 @@ public void stateChanged(ChangeEvent e) jSplitPane1.add(panelTop, JSplitPane.TOP); jSplitPane1.add(panelBottom, JSplitPane.BOTTOM); - /* - border4 = BorderFactory.createBevelBorder(BevelBorder.LOWERED, Color.white, - Color.white, new Color(93, 93, 93), new Color(134, 134, 134)); - */ contentPane = (JPanel) this.getContentPane(); contentPane.setLayout(contentLayout); contentPane.setPreferredSize(new Dimension(601, 690)); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/RelatePanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/RelatePanel.java index 4db24c3500..bf94202278 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/RelatePanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/RelatePanel.java @@ -263,20 +263,20 @@ void jbInit() throws Exception { relateIB.setText("F"); jPanel1.setLayout(gridBagLayout2); jLabel14.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel14.setForeground(AppColors.A); + jLabel14.setForeground(AppColors.GEOM_A); jLabel14.setText(AppStrings.GEOM_LABEL_A); jLabel13.setFont(new java.awt.Font("Dialog", 2, 12)); - jLabel13.setForeground(AppColors.A); + jLabel13.setForeground(AppColors.GEOM_A); jLabel13.setText("Ext"); jLabel12.setFont(new java.awt.Font("Dialog", 2, 12)); - jLabel12.setForeground(AppColors.A); + jLabel12.setForeground(AppColors.GEOM_A); jLabel12.setText("Bdy"); jLabel11.setFont(new java.awt.Font("Dialog", 2, 12)); - jLabel11.setForeground(AppColors.A); + jLabel11.setForeground(AppColors.GEOM_A); jLabel11.setToolTipText(""); jLabel11.setText("Int"); jLabel10.setFont(new java.awt.Font("Dialog", 2, 12)); - jLabel10.setForeground(AppColors.B); + jLabel10.setForeground(AppColors.GEOM_B); jLabel10.setToolTipText(""); jLabel10.setText("Ext"); txtAB.setBackground(AppColors.BACKGROUND); @@ -288,23 +288,23 @@ void jbInit() throws Exception { txtAB.setEditable(false); txtAB.setHorizontalAlignment(SwingConstants.LEFT); jLabel23.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel23.setForeground(AppColors.B); + jLabel23.setForeground(AppColors.GEOM_B); jLabel23.setText(AppStrings.GEOM_LABEL_B); relateBI.setFont(new java.awt.Font("Dialog", 1, 12)); relateBI.setText("F"); jLabel22.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel22.setForeground(AppColors.A); + jLabel22.setForeground(AppColors.GEOM_A); jLabel22.setToolTipText(""); jLabel22.setText(AppStrings.GEOM_LABEL_A); relateEI.setFont(new java.awt.Font("Dialog", 1, 12)); relateEI.setText("F"); jLabel21.setToolTipText(""); jLabel21.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel21.setForeground(AppColors.A); + jLabel21.setForeground(AppColors.GEOM_A); jLabel21.setToolTipText(""); jLabel21.setText(AppStrings.GEOM_LABEL_A); jLabel20.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel20.setForeground(AppColors.B); + jLabel20.setForeground(AppColors.GEOM_B); jLabel20.setToolTipText(""); jLabel20.setText(AppStrings.GEOM_LABEL_B); relateBE.setFont(new java.awt.Font("Dialog", 1, 12)); @@ -321,17 +321,17 @@ void jbInit() throws Exception { relateBB.setFont(new java.awt.Font("Dialog", 1, 12)); relateBB.setText("F"); jLabel9.setFont(new java.awt.Font("Dialog", 2, 12)); - jLabel9.setForeground(AppColors.B); + jLabel9.setForeground(AppColors.GEOM_B); jLabel9.setToolTipText(""); jLabel9.setText("Bdy"); relateEB.setFont(new java.awt.Font("Dialog", 1, 12)); relateEB.setText("F"); jLabel8.setFont(new java.awt.Font("Dialog", 2, 12)); - jLabel8.setForeground(AppColors.B); + jLabel8.setForeground(AppColors.GEOM_B); jLabel8.setToolTipText(""); jLabel8.setText("Int"); jLabel7.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel7.setForeground(AppColors.B); + jLabel7.setForeground(AppColors.GEOM_B); jLabel7.setText(AppStrings.GEOM_LABEL_B); relateII.setBackground(Color.white); relateII.setFont(new java.awt.Font("Dialog", 1, 12)); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/ValidPanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/ValidPanel.java index 6f4968c038..c245d06698 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/ValidPanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/ValidPanel.java @@ -111,10 +111,11 @@ public void actionPerformed(ActionEvent e) { rbA.setSelected(true); rbA.setText(AppStrings.GEOM_LABEL_A); - rbA.setForeground(AppColors.A); + rbA.setForeground(AppColors.GEOM_A); rbB.setText(AppStrings.GEOM_LABEL_B); - rbB.setForeground(AppColors.B); + rbB.setForeground(AppColors.GEOM_B); rbResult.setText(AppStrings.GEOM_LABEL_RESULT); + rbResult.setForeground(AppColors.GEOM_RESULT); rbA.addItemListener(new ItemListener() { @Override diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/WKTPanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/WKTPanel.java index 76e63960bd..6f86ea9f4d 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/WKTPanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/WKTPanel.java @@ -150,13 +150,13 @@ public void actionPerformed(ActionEvent e) { panelAB.setLayout(gridBagLayout2); aLabel.setFont(new java.awt.Font("Dialog", 1, 16)); - aLabel.setForeground(AppColors.A); + aLabel.setForeground(AppColors.GEOM_A); aLabel.setText(AppStrings.GEOM_LABEL_A); aLabel.setPreferredSize(new Dimension(20, 20)); aLabel.setHorizontalTextPosition(SwingConstants.LEFT); bLabel.setFont(new java.awt.Font("Dialog", 1, 16)); - bLabel.setForeground(AppColors.B); + bLabel.setForeground(AppColors.GEOM_B); bLabel.setText(AppStrings.GEOM_LABEL_B); bLabel.setPreferredSize(new Dimension(20, 20)); @@ -501,7 +501,7 @@ public void filesDropped(java.io.File[] files) { //Border otherBorder = BorderFactory.createEmptyBorder(); Border otherBorder = BorderFactory.createMatteBorder(0, 2, 0, 0, Color.white); - private static Color focusBackgroundColor = Color.white; //new Color(240,255,250); + private static Color focusBackgroundColor = AppColors.BACKGROUND_FOCUS; private static Color otherBackgroundColor = AppColors.BACKGROUND; private void setFocusGeometry(int index) { diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/TestBuilderModel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/TestBuilderModel.java index 5d09c5ca04..c084d2eb36 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/TestBuilderModel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/TestBuilderModel.java @@ -30,9 +30,9 @@ import org.locationtech.jts.util.Assert; import org.locationtech.jtstest.test.TestCaseList; import org.locationtech.jtstest.test.Testable; +import org.locationtech.jtstest.testbuilder.AppColors; import org.locationtech.jtstest.testbuilder.AppConstants; import org.locationtech.jtstest.testbuilder.AppStrings; -import org.locationtech.jtstest.testbuilder.GeometryDepiction; import org.locationtech.jtstest.testbuilder.ui.SwingUtil; import org.locationtech.jtstest.testbuilder.ui.style.BasicStyle; import org.locationtech.jtstest.testrunner.TestReader; @@ -165,16 +165,16 @@ private void initLayers() new ResultGeometryContainer(geomEditModel)); Layer lyrA = layerList.getLayer(LayerList.LYR_A); - lyrA.setGeometryStyle(new BasicStyle(GeometryDepiction.GEOM_A_LINE_CLR, - GeometryDepiction.GEOM_A_FILL_CLR)); + lyrA.setGeometryStyle(new BasicStyle(AppColors.GEOM_A_LINE_CLR, + AppColors.GEOM_A_FILL_CLR)); Layer lyrB = layerList.getLayer(LayerList.LYR_B); - lyrB.setGeometryStyle(new BasicStyle(GeometryDepiction.GEOM_B_LINE_CLR, - GeometryDepiction.GEOM_B_FILL_CLR)); + lyrB.setGeometryStyle(new BasicStyle(AppColors.GEOM_B_LINE_CLR, + AppColors.GEOM_B_FILL_CLR)); Layer lyrR = layerList.getLayer(LayerList.LYR_RESULT); - lyrR.setGeometryStyle(new BasicStyle(GeometryDepiction.GEOM_RESULT_LINE_CLR, - GeometryDepiction.GEOM_RESULT_FILL_CLR)); + lyrR.setGeometryStyle(new BasicStyle(AppColors.GEOM_RESULT_LINE_CLR, + AppColors.GEOM_RESULT_FILL_CLR)); } public void pasteGeometry(int geomIndex) throws Exception { From 85fe56d377c981545023cc1620b54fb384e9c5c5 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 7 Dec 2023 10:17:41 -0800 Subject: [PATCH 051/123] Renove unneeded precision from XML tests --- .../tests/src/test/resources/testxml/general/TestRelateAA.xml | 1 - .../tests/src/test/resources/testxml/general/TestRelateLA.xml | 1 - .../tests/src/test/resources/testxml/general/TestRelatePA.xml | 1 - .../tests/src/test/resources/testxml/general/TestRelatePL.xml | 1 - .../tests/src/test/resources/testxml/general/TestRelatePP.xml | 1 - 5 files changed, 5 deletions(-) diff --git a/modules/tests/src/test/resources/testxml/general/TestRelateAA.xml b/modules/tests/src/test/resources/testxml/general/TestRelateAA.xml index bbee510c89..b73c12d077 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRelateAA.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRelateAA.xml @@ -1,5 +1,4 @@ - AA disjoint diff --git a/modules/tests/src/test/resources/testxml/general/TestRelateLA.xml b/modules/tests/src/test/resources/testxml/general/TestRelateLA.xml index acc895468f..482dc7b2a4 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRelateLA.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRelateLA.xml @@ -1,5 +1,4 @@ - LA - intersection at NV: {A-Bdy, A-Int} = {B-Bdy, B-Int} diff --git a/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml b/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml index 642d682b72..07ceb7e933 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml @@ -1,5 +1,4 @@ - PA - disjoint diff --git a/modules/tests/src/test/resources/testxml/general/TestRelatePL.xml b/modules/tests/src/test/resources/testxml/general/TestRelatePL.xml index 13bb4dc095..07ddff57ac 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRelatePL.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRelatePL.xml @@ -1,5 +1,4 @@ - PL - disjoint diff --git a/modules/tests/src/test/resources/testxml/general/TestRelatePP.xml b/modules/tests/src/test/resources/testxml/general/TestRelatePP.xml index 738978b173..10bdf362be 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRelatePP.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRelatePP.xml @@ -1,5 +1,4 @@ - same point From 642e9277ef0d6d4e95a6bc674ad257d5a98c255f Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 7 Dec 2023 10:24:35 -0800 Subject: [PATCH 052/123] Remove unnecessary precision from XML tests --- .../tests/src/test/resources/testxml/general/TestBoundary.xml | 1 - .../tests/src/test/resources/testxml/general/TestCentroid.xml | 1 - .../src/test/resources/testxml/general/TestConvexHull-big.xml | 1 - .../tests/src/test/resources/testxml/general/TestDistance.xml | 1 - .../tests/src/test/resources/testxml/general/TestPolygonize.xml | 1 - .../test/resources/testxml/general/TestRectanglePredicate.xml | 1 - modules/tests/src/test/resources/testxml/general/TestSimple.xml | 1 - modules/tests/src/test/resources/testxml/general/TestValid.xml | 1 - .../src/test/resources/testxml/general/TestWithinDistance.xml | 1 - 9 files changed, 9 deletions(-) diff --git a/modules/tests/src/test/resources/testxml/general/TestBoundary.xml b/modules/tests/src/test/resources/testxml/general/TestBoundary.xml index befea95c30..f3a6dff809 100644 --- a/modules/tests/src/test/resources/testxml/general/TestBoundary.xml +++ b/modules/tests/src/test/resources/testxml/general/TestBoundary.xml @@ -1,5 +1,4 @@ - P - point diff --git a/modules/tests/src/test/resources/testxml/general/TestCentroid.xml b/modules/tests/src/test/resources/testxml/general/TestCentroid.xml index c078e8afde..009ccd5c0d 100644 --- a/modules/tests/src/test/resources/testxml/general/TestCentroid.xml +++ b/modules/tests/src/test/resources/testxml/general/TestCentroid.xml @@ -1,5 +1,4 @@ - P - empty diff --git a/modules/tests/src/test/resources/testxml/general/TestConvexHull-big.xml b/modules/tests/src/test/resources/testxml/general/TestConvexHull-big.xml index 7de9aa2935..7651a611ad 100644 --- a/modules/tests/src/test/resources/testxml/general/TestConvexHull-big.xml +++ b/modules/tests/src/test/resources/testxml/general/TestConvexHull-big.xml @@ -1,5 +1,4 @@ - Big convex hull diff --git a/modules/tests/src/test/resources/testxml/general/TestDistance.xml b/modules/tests/src/test/resources/testxml/general/TestDistance.xml index 3835c919da..e6444ddac4 100644 --- a/modules/tests/src/test/resources/testxml/general/TestDistance.xml +++ b/modules/tests/src/test/resources/testxml/general/TestDistance.xml @@ -1,5 +1,4 @@ - PeP - point to an empty point diff --git a/modules/tests/src/test/resources/testxml/general/TestPolygonize.xml b/modules/tests/src/test/resources/testxml/general/TestPolygonize.xml index 69f80339e4..4d5f9b73d1 100644 --- a/modules/tests/src/test/resources/testxml/general/TestPolygonize.xml +++ b/modules/tests/src/test/resources/testxml/general/TestPolygonize.xml @@ -1,5 +1,4 @@ - P - single point diff --git a/modules/tests/src/test/resources/testxml/general/TestRectanglePredicate.xml b/modules/tests/src/test/resources/testxml/general/TestRectanglePredicate.xml index 254f1e7518..38b9d6ca28 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRectanglePredicate.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRectanglePredicate.xml @@ -1,5 +1,4 @@ - A disjoint diff --git a/modules/tests/src/test/resources/testxml/general/TestSimple.xml b/modules/tests/src/test/resources/testxml/general/TestSimple.xml index a75cdb1f81..c496ab7d94 100644 --- a/modules/tests/src/test/resources/testxml/general/TestSimple.xml +++ b/modules/tests/src/test/resources/testxml/general/TestSimple.xml @@ -1,5 +1,4 @@ - P - point diff --git a/modules/tests/src/test/resources/testxml/general/TestValid.xml b/modules/tests/src/test/resources/testxml/general/TestValid.xml index 7173219169..dc94b3efde 100644 --- a/modules/tests/src/test/resources/testxml/general/TestValid.xml +++ b/modules/tests/src/test/resources/testxml/general/TestValid.xml @@ -1,5 +1,4 @@ - P - point (valid) diff --git a/modules/tests/src/test/resources/testxml/general/TestWithinDistance.xml b/modules/tests/src/test/resources/testxml/general/TestWithinDistance.xml index a4a1e3511f..1438bcd279 100644 --- a/modules/tests/src/test/resources/testxml/general/TestWithinDistance.xml +++ b/modules/tests/src/test/resources/testxml/general/TestWithinDistance.xml @@ -1,5 +1,4 @@ - PP - disjoint points From eaa6d1d91c548b525629b926caed1df1c5c35f4a Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 7 Dec 2023 10:31:18 -0800 Subject: [PATCH 053/123] Remove unecessary precision from XML tests --- .../src/test/resources/testxml/failure/TestBufferFailure.xml | 2 +- .../resources/testxml/failure/TestReducePrecisionFailure.xml | 2 +- modules/tests/src/test/resources/testxml/general/TestBuffer.xml | 2 +- .../src/test/resources/testxml/general/TestBufferMitredJoin.xml | 2 +- .../tests/src/test/resources/testxml/general/TestDensify.xml | 2 +- .../src/test/resources/testxml/general/TestMinimumClearance.xml | 2 +- .../src/test/resources/testxml/misc/TestBufferExternal.xml | 2 +- .../src/test/resources/testxml/misc/geos-bug356-buffer.xml | 2 +- .../robust/overlay/TestOverlay-geos-1046-union-lines.xml | 2 +- .../resources/testxml/robust/overlay/TestOverlay-geos-234.xml | 2 +- .../resources/testxml/robust/overlay/TestOverlay-geos-275.xml | 2 +- .../resources/testxml/robust/overlay/TestOverlay-geos-350.xml | 2 +- .../resources/testxml/robust/overlay/TestOverlay-geos-358.xml | 2 +- .../resources/testxml/robust/overlay/TestOverlay-geos-360.xml | 2 +- .../testxml/robust/overlay/TestOverlay-geos-392-lines.xml | 2 +- .../resources/testxml/robust/overlay/TestOverlay-geos-392.xml | 2 +- .../resources/testxml/robust/overlay/TestOverlay-geos-398.xml | 2 +- .../resources/testxml/robust/overlay/TestOverlay-geos-459.xml | 2 +- .../resources/testxml/robust/overlay/TestOverlay-geos-488.xml | 2 +- .../resources/testxml/robust/overlay/TestOverlay-geos-522.xml | 2 +- .../resources/testxml/robust/overlay/TestOverlay-geos-527.xml | 2 +- .../resources/testxml/robust/overlay/TestOverlay-geos-599.xml | 2 +- .../testxml/robust/overlay/TestOverlay-geos-600-lines.xml | 2 +- .../resources/testxml/robust/overlay/TestOverlay-geos-615.xml | 2 +- .../resources/testxml/robust/overlay/TestOverlay-geos-994.xml | 2 +- .../resources/testxml/robust/overlay/TestOverlay-misc-3.xml | 2 +- .../resources/testxml/robust/overlay/TestOverlay-misc-4.xml | 2 +- .../resources/testxml/robust/overlay/TestOverlay-pg-2055.xml | 2 +- .../resources/testxml/robust/overlay/TestOverlay-pg-2176.xml | 2 +- .../resources/testxml/robust/overlay/TestOverlay-pg-4182-2.xml | 2 +- .../resources/testxml/robust/overlay/TestOverlay-pg-4538.xml | 2 +- .../resources/testxml/robust/overlay/TestOverlay-pg-4738.xml | 2 +- .../testxml/robust/overlay/TestOverlay-shapely-829.xml | 2 +- 33 files changed, 33 insertions(+), 33 deletions(-) diff --git a/modules/tests/src/test/resources/testxml/failure/TestBufferFailure.xml b/modules/tests/src/test/resources/testxml/failure/TestBufferFailure.xml index 0172730256..1f408ec25a 100644 --- a/modules/tests/src/test/resources/testxml/failure/TestBufferFailure.xml +++ b/modules/tests/src/test/resources/testxml/failure/TestBufferFailure.xml @@ -2,7 +2,7 @@ Various cases which have been reported or identified as causing buffer failures. - + com.vividsolutions.jtstest.testrunner.BufferResultMatcher diff --git a/modules/tests/src/test/resources/testxml/failure/TestReducePrecisionFailure.xml b/modules/tests/src/test/resources/testxml/failure/TestReducePrecisionFailure.xml index 0df4c7472b..a7aafee202 100644 --- a/modules/tests/src/test/resources/testxml/failure/TestReducePrecisionFailure.xml +++ b/modules/tests/src/test/resources/testxml/failure/TestReducePrecisionFailure.xml @@ -5,7 +5,7 @@ See https://github.com/libgeos/geos/issues/511 Cases cause a TopologyException. Expected result is just a placeholder - + This used to fail, but with a fix to SnapRoundingNoder now works diff --git a/modules/tests/src/test/resources/testxml/general/TestBuffer.xml b/modules/tests/src/test/resources/testxml/general/TestBuffer.xml index 49b5dc238b..0cb3999e1b 100644 --- a/modules/tests/src/test/resources/testxml/general/TestBuffer.xml +++ b/modules/tests/src/test/resources/testxml/general/TestBuffer.xml @@ -2,7 +2,7 @@ Basic buffer test cases. - + org.locationtech.jtstest.testrunner.BufferResultMatcher diff --git a/modules/tests/src/test/resources/testxml/general/TestBufferMitredJoin.xml b/modules/tests/src/test/resources/testxml/general/TestBufferMitredJoin.xml index b266343fc0..d3a4046c1d 100644 --- a/modules/tests/src/test/resources/testxml/general/TestBufferMitredJoin.xml +++ b/modules/tests/src/test/resources/testxml/general/TestBufferMitredJoin.xml @@ -2,7 +2,7 @@ Test cases for buffers with mitred joins. - + org.locationtech.jtstest.testrunner.BufferResultMatcher diff --git a/modules/tests/src/test/resources/testxml/general/TestDensify.xml b/modules/tests/src/test/resources/testxml/general/TestDensify.xml index 9945e63f7d..8a8a124fe3 100644 --- a/modules/tests/src/test/resources/testxml/general/TestDensify.xml +++ b/modules/tests/src/test/resources/testxml/general/TestDensify.xml @@ -1,5 +1,5 @@ - + P - single point diff --git a/modules/tests/src/test/resources/testxml/general/TestMinimumClearance.xml b/modules/tests/src/test/resources/testxml/general/TestMinimumClearance.xml index d770180bb9..4534811190 100644 --- a/modules/tests/src/test/resources/testxml/general/TestMinimumClearance.xml +++ b/modules/tests/src/test/resources/testxml/general/TestMinimumClearance.xml @@ -1,5 +1,5 @@ - + P - empty point diff --git a/modules/tests/src/test/resources/testxml/misc/TestBufferExternal.xml b/modules/tests/src/test/resources/testxml/misc/TestBufferExternal.xml index 27d22b827b..c0f0857953 100644 --- a/modules/tests/src/test/resources/testxml/misc/TestBufferExternal.xml +++ b/modules/tests/src/test/resources/testxml/misc/TestBufferExternal.xml @@ -4,7 +4,7 @@ in previous versions of JTS. The cases in this file should all pass in the current version of JTS. - + com.vividsolutions.jtstest.testrunner.BufferResultMatcher diff --git a/modules/tests/src/test/resources/testxml/misc/geos-bug356-buffer.xml b/modules/tests/src/test/resources/testxml/misc/geos-bug356-buffer.xml index 8a7449497c..650225fd8a 100644 --- a/modules/tests/src/test/resources/testxml/misc/geos-bug356-buffer.xml +++ b/modules/tests/src/test/resources/testxml/misc/geos-bug356-buffer.xml @@ -2,7 +2,7 @@ http://trac.osgeo.org/geos/ticket/356 - + com.vividsolutions.jtstest.testrunner.BufferResultMatcher diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-1046-union-lines.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-1046-union-lines.xml index 3060558223..12aaf6f699 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-1046-union-lines.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-1046-union-lines.xml @@ -1,5 +1,5 @@ - + .01 diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-234.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-234.xml index ad25099a8a..91000d1ccd 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-234.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-234.xml @@ -2,7 +2,7 @@ http://trac.osgeo.org/geos/ticket/234 - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-275.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-275.xml index 5d3f2dc050..3fff93dd6d 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-275.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-275.xml @@ -2,7 +2,7 @@ http://trac.osgeo.org/geos/ticket/275 - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-350.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-350.xml index a50f61a03e..1546b35f06 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-350.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-350.xml @@ -1,6 +1,6 @@ http://trac.osgeo.org/geos/ticket/350 - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-358.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-358.xml index 7c6f353a72..2ceb0bd273 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-358.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-358.xml @@ -2,7 +2,7 @@ http://trac.osgeo.org/geos/ticket/358 - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-360.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-360.xml index 1ce0cc64ac..8545067a90 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-360.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-360.xml @@ -2,7 +2,7 @@ http://trac.osgeo.org/geos/ticket/360 - + 1 diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-392-lines.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-392-lines.xml index d15d275cf6..cd6b8d3845 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-392-lines.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-392-lines.xml @@ -2,7 +2,7 @@ http://trac.osgeo.org/geos/ticket/392 - + .01 diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-392.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-392.xml index 49267e606b..2cc7c34df8 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-392.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-392.xml @@ -2,7 +2,7 @@ http://trac.osgeo.org/geos/ticket/392 - + .01 diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-398.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-398.xml index 84179f5ab8..7e8bde84bf 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-398.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-398.xml @@ -2,7 +2,7 @@ http://trac.osgeo.org/geos/ticket/398 - + http://trac.osgeo.org/geos/ticket/459 - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-488.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-488.xml index d1a897dc61..fb2fa142c0 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-488.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-488.xml @@ -3,7 +3,7 @@ http://trac.osgeo.org/geos/ticket/488 - + 1E-7 diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-522.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-522.xml index 13acd0ee25..0875192852 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-522.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-522.xml @@ -1,6 +1,6 @@ - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-527.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-527.xml index c3d77da550..e81cddebce 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-527.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-527.xml @@ -1,6 +1,6 @@ - + .01 diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-599.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-599.xml index f947be5273..ec8b397c9d 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-599.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-599.xml @@ -2,7 +2,7 @@ http://trac.osgeo.org/geos/ticket/599 - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-600-lines.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-600-lines.xml index f5df9d2e11..d92f348d74 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-600-lines.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-600-lines.xml @@ -1,5 +1,5 @@ - + .01 diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-615.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-615.xml index e0a4b44c60..5eaec4bab3 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-615.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-615.xml @@ -1,6 +1,6 @@ - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-994.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-994.xml index 50342c5a75..4b643980dd 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-994.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-994.xml @@ -1,5 +1,5 @@ - + 1 Unary union test from QGIS test suite. diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-misc-3.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-misc-3.xml index 5cc8068b16..24d83f8bfb 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-misc-3.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-misc-3.xml @@ -1,5 +1,5 @@ - + AA - OLD robustness failure (works with snapping) diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-misc-4.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-misc-4.xml index c52bb1004f..a1558a662f 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-misc-4.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-misc-4.xml @@ -1,5 +1,5 @@ - + AA - causes failure due to snapping making input invalid (JTS 1.10) diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-2055.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-2055.xml index 0dce60417e..560ff45e32 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-2055.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-2055.xml @@ -1,5 +1,5 @@ - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-2176.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-2176.xml index 0a0719117f..a983fdaea0 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-2176.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-2176.xml @@ -1,5 +1,5 @@ - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4182-2.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4182-2.xml index c76d0e15af..5640070a66 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4182-2.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4182-2.xml @@ -3,7 +3,7 @@ https://trac.osgeo.org/postgis/ticket/4182 Overlay Topology Exception - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4538.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4538.xml index 6e4c478fa2..2fb33c658b 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4538.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4538.xml @@ -3,7 +3,7 @@ https://trac.osgeo.org/postgis/ticket/4538 Overlay Topology Exception - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4738.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4738.xml index 256bb4aebd..5357e72432 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4738.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4738.xml @@ -3,7 +3,7 @@ https://trac.osgeo.org/postgis/ticket/4738 Union of LineStrings fails using simple noding. - + 1E-12 diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-shapely-829.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-shapely-829.xml index ec3fabc6c0..c3cc841ba6 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-shapely-829.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-shapely-829.xml @@ -3,7 +3,7 @@ https://github.com/Toblerity/Shapely/issues/829 Overlay Topology Exception on union of polygons - + 0.01 From 9fd9c487f6c57ee9465bcaaaa301c8ee689cf2e3 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 11 Dec 2023 14:46:26 -0800 Subject: [PATCH 054/123] Remove unnecessary precision from XML test --- .../resources/testxml/general/TestUnaryUnion.xml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/modules/tests/src/test/resources/testxml/general/TestUnaryUnion.xml b/modules/tests/src/test/resources/testxml/general/TestUnaryUnion.xml index 090477fc86..c8f333fda4 100644 --- a/modules/tests/src/test/resources/testxml/general/TestUnaryUnion.xml +++ b/modules/tests/src/test/resources/testxml/general/TestUnaryUnion.xml @@ -1,6 +1,5 @@ Tests for Geometry.union() method (unary union) - P - point (showing merging of identical points) @@ -144,18 +143,6 @@ - - mA - multipolygon (invalid) with topology collapse - MULTIPOLYGON (((0 0, 150 0, 150 1, 0 0)), - ((180 0, 20 0, 20 100, 180 100, 180 0))) - - - - POLYGON ((150 0, 20 0, 20 100, 180 100, 180 0, 150 0)) - - - - P - empty Point POINT EMPTY From 59f6482439a4eb63237db40794047440a42e9613 Mon Sep 17 00:00:00 2001 From: Michael Barry Date: Tue, 2 Jan 2024 14:55:21 -0500 Subject: [PATCH 055/123] Improve performance of HPRtree (#1012) Signed-off-by: Mike Barry --- .../jts/index/hprtree/HPRtree.java | 195 +++++++++--------- .../locationtech/jts/noding/MCIndexNoder.java | 6 +- .../noding/snapround/MCIndexPointSnapper.java | 5 +- .../test/jts/perf/index/FlatbushPerfTest.java | 121 +++++++++++ 4 files changed, 223 insertions(+), 104 deletions(-) create mode 100644 modules/core/src/test/java/test/jts/perf/index/FlatbushPerfTest.java diff --git a/modules/core/src/main/java/org/locationtech/jts/index/hprtree/HPRtree.java b/modules/core/src/main/java/org/locationtech/jts/index/hprtree/HPRtree.java index f596673a8b..f6f166712a 100644 --- a/modules/core/src/main/java/org/locationtech/jts/index/hprtree/HPRtree.java +++ b/modules/core/src/main/java/org/locationtech/jts/index/hprtree/HPRtree.java @@ -12,16 +12,14 @@ package org.locationtech.jts.index.hprtree; import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.List; import org.locationtech.jts.geom.Envelope; -import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.index.ArrayListVisitor; import org.locationtech.jts.index.ItemVisitor; import org.locationtech.jts.index.SpatialIndex; import org.locationtech.jts.index.strtree.STRtree; +import org.locationtech.jts.util.IntArrayList; /** * A Hilbert-Packed R-tree. This is a static R-tree @@ -59,28 +57,32 @@ * @author Martin Davis * */ -public class HPRtree +public class HPRtree implements SpatialIndex { private static final int ENV_SIZE = 4; private static final int HILBERT_LEVEL = 12; - private static int DEFAULT_NODE_CAPACITY = 16; + private static final int DEFAULT_NODE_CAPACITY = 16; - private List items = new ArrayList(); - - private int nodeCapacity = DEFAULT_NODE_CAPACITY; + private List itemsToLoad = new ArrayList<>(); + + private final int nodeCapacity; + + private int numItems = 0; - private Envelope totalExtent = new Envelope(); + private final Envelope totalExtent = new Envelope(); private int[] layerStartIndex; private double[] nodeBounds; - private boolean isBuilt = false; + private double[] itemBounds; - //public int nodeIntersectsCount; + private Object[] itemValues; + + private volatile boolean isBuilt = false; /** * Creates a new index with the default node capacity. @@ -104,7 +106,7 @@ public HPRtree(int nodeCapacity) { * @return the number of items */ public int size() { - return items.size(); + return numItems; } @Override @@ -112,7 +114,8 @@ public void insert(Envelope itemEnv, Object item) { if (isBuilt) { throw new IllegalStateException("Cannot insert items after tree is built."); } - items.add( new Item(itemEnv, item) ); + numItems++; + itemsToLoad.add( new Item(itemEnv, item) ); totalExtent.expandToInclude(itemEnv); } @@ -153,7 +156,7 @@ private void queryTopLayer(Envelope searchEnv, ItemVisitor visitor) { private void queryNode(int layerIndex, int nodeOffset, Envelope searchEnv, ItemVisitor visitor) { int layerStart = layerStartIndex[layerIndex]; int nodeIndex = layerStart + nodeOffset; - if (! intersects(nodeIndex, searchEnv)) return; + if (! intersects(nodeBounds, nodeIndex, searchEnv)) return; if (layerIndex == 0) { int childNodesOffset = nodeOffset / ENV_SIZE * nodeCapacity; queryItems(childNodesOffset, searchEnv, visitor); @@ -164,12 +167,11 @@ private void queryNode(int layerIndex, int nodeOffset, Envelope searchEnv, ItemV } } - private boolean intersects(int nodeIndex, Envelope env) { - //nodeIntersectsCount++; - boolean isBeyond = (env.getMaxX() < nodeBounds[nodeIndex]) - || (env.getMaxY() < nodeBounds[nodeIndex+1]) - || (env.getMinX() > nodeBounds[nodeIndex+2]) - || (env.getMinY() > nodeBounds[nodeIndex+3]); + private static boolean intersects(double[] bounds, int nodeIndex, Envelope env) { + boolean isBeyond = (env.getMaxX() < bounds[nodeIndex]) + || (env.getMaxY() < bounds[nodeIndex+1]) + || (env.getMinX() > bounds[nodeIndex+2]) + || (env.getMinY() > bounds[nodeIndex+3]); return ! isBeyond; } @@ -187,34 +189,14 @@ private void queryNodeChildren(int layerIndex, int blockOffset, Envelope searchE private void queryItems(int blockStart, Envelope searchEnv, ItemVisitor visitor) { for (int i = 0; i < nodeCapacity; i++) { - int itemIndex = blockStart + i; + int itemIndex = blockStart + i; // don't query past end of items - if (itemIndex >= items.size()) break; - - // visit the item if its envelope intersects search env - Item item = items.get(itemIndex); - //nodeIntersectsCount++; - if (intersects( item.getEnvelope(), searchEnv) ) { - //if (item.getEnvelope().intersects(searchEnv)) { - visitor.visitItem(item.getItem()); + if (itemIndex >= numItems) break; + if (intersects(itemBounds, itemIndex * ENV_SIZE, searchEnv)) { + visitor.visitItem(itemValues[itemIndex]); } } } - - /** - * Tests whether two envelopes intersect. - * Avoids the null check in {@link Envelope#intersects(Envelope)}. - * - * @param env1 an envelope - * @param env2 an envelope - * @return true if the envelopes intersect - */ - private static boolean intersects(Envelope env1, Envelope env2) { - return !(env2.getMinX() > env1.getMaxX() || - env2.getMaxX() < env1.getMinX() || - env2.getMinY() > env1.getMaxY() || - env2.getMaxY() < env1.getMinY()); - } private int layerSize(int layerIndex) { int layerStart = layerStartIndex[layerIndex]; @@ -231,47 +213,55 @@ public boolean remove(Envelope itemEnv, Object item) { /** * Builds the index, if not already built. */ - public synchronized void build() { + public void build() { // skip if already built - if (isBuilt) return; - isBuilt = true; + if (!isBuilt) { + synchronized (this) { + if (!isBuilt) { + prepareIndex(); + prepareItems(); + this.isBuilt = true; + } + } + } + } + + private void prepareIndex() { // don't need to build an empty or very small tree - if (items.size() <= nodeCapacity) return; + if (itemsToLoad.size() <= nodeCapacity) return; sortItems(); - //dumpItems(items); - - layerStartIndex = computeLayerIndices(items.size(), nodeCapacity); + + layerStartIndex = computeLayerIndices(numItems, nodeCapacity); // allocate storage int nodeCount = layerStartIndex[ layerStartIndex.length - 1 ] / 4; nodeBounds = createBoundsArray(nodeCount); - + // compute tree nodes computeLeafNodes(layerStartIndex[1]); for (int i = 1; i < layerStartIndex.length - 1; i++) { computeLayerNodes(i); } - //dumpNodes(); } - /* - private void dumpNodes() { - GeometryFactory fact = new GeometryFactory(); - for (int i = 0; i < nodeMinX.length; i++) { - Envelope env = new Envelope(nodeMinX[i], nodeMaxX[i], nodeMinY[i], nodeMaxY[i]);; - System.out.println(fact.toGeometry(env)); + private void prepareItems() { + // copy item contents out to arrays for querying + int boundsIndex = 0; + int valueIndex = 0; + itemBounds = new double[itemsToLoad.size() * 4]; + itemValues = new Object[itemsToLoad.size()]; + for (Item item : itemsToLoad) { + Envelope envelope = item.getEnvelope(); + itemBounds[boundsIndex++] = envelope.getMinX(); + itemBounds[boundsIndex++] = envelope.getMinY(); + itemBounds[boundsIndex++] = envelope.getMaxX(); + itemBounds[boundsIndex++] = envelope.getMaxY(); + itemValues[valueIndex++] = item.getItem(); } + // and let GC free the original list + itemsToLoad = null; } - private static void dumpItems(List items) { - GeometryFactory fact = new GeometryFactory(); - for (Item item : items) { - Envelope env = item.getEnvelope(); - System.out.println(fact.toGeometry(env)); - } - } - */ - private static double[] createBoundsArray(int size) { double[] a = new double[4*size]; for (int i = 0; i < size; i++) { @@ -292,7 +282,6 @@ private void computeLayerNodes(int layerIndex) { for (int i = 0; i < layerSize; i += ENV_SIZE) { int childStart = childLayerStart + nodeCapacity * i; computeNodeBounds(layerStart + i, childStart, childLayerEnd); - //System.out.println("Layer: " + layerIndex + " node: " + i + " - " + getNodeEnvelope(layerStart + i)); } } @@ -313,8 +302,8 @@ private void computeLeafNodes(int layerSize) { private void computeLeafNodeBounds(int nodeIndex, int blockStart) { for (int i = 0; i <= nodeCapacity; i++ ) { int itemIndex = blockStart + i; - if (itemIndex >= items.size()) break; - Envelope env = items.get(itemIndex).getEnvelope(); + if (itemIndex >= itemsToLoad.size()) break; + Envelope env = itemsToLoad.get(itemIndex).getEnvelope(); updateNodeBounds(nodeIndex, env.getMinX(), env.getMinY(), env.getMaxX(), env.getMaxY()); } } @@ -325,13 +314,9 @@ private void updateNodeBounds(int nodeIndex, double minX, double minY, double ma if (maxX > nodeBounds[nodeIndex+2]) nodeBounds[nodeIndex+2] = maxX; if (maxY > nodeBounds[nodeIndex+3]) nodeBounds[nodeIndex+3] = maxY; } - - private Envelope getNodeEnvelope(int i) { - return new Envelope(nodeBounds[i], nodeBounds[i+1], nodeBounds[i+2], nodeBounds[i+3]); - } private static int[] computeLayerIndices(int itemSize, int nodeCapacity) { - List layerIndexList = new ArrayList(); + IntArrayList layerIndexList = new IntArrayList(); int layerSize = itemSize; int index = 0; do { @@ -339,7 +324,7 @@ private static int[] computeLayerIndices(int itemSize, int nodeCapacity) { layerSize = numNodesToCover(layerSize, nodeCapacity); index += ENV_SIZE * layerSize; } while (layerSize > 1); - return toIntArray(layerIndexList); + return layerIndexList.toArray(); } /** @@ -356,14 +341,6 @@ private static int numNodesToCover(int nChild, int nodeCapacity) { if (total == nChild) return mult; return mult + 1; } - - private static int[] toIntArray(List list) { - int[] array = new int[list.size()]; - for (int i = 0; i < array.length; i++) { - array[i] = list.get(i); - } - return array; - } /** * Gets the extents of the internal index nodes @@ -383,24 +360,46 @@ public Envelope[] getBounds() { } private void sortItems() { - ItemComparator comp = new ItemComparator(new HilbertEncoder(HILBERT_LEVEL, totalExtent)); - Collections.sort(items, comp); + HilbertEncoder encoder = new HilbertEncoder(HILBERT_LEVEL, totalExtent); + int[] hilbertValues = new int[itemsToLoad.size()]; + int pos = 0; + for (Item item : itemsToLoad) { + hilbertValues[pos++] = encoder.encode(item.getEnvelope()); + } + quickSortItemsIntoNodes(hilbertValues, 0, itemsToLoad.size() - 1); } - - static class ItemComparator implements Comparator { - private HilbertEncoder encoder; - - public ItemComparator(HilbertEncoder encoder) { - this.encoder = encoder; + private void quickSortItemsIntoNodes(int[] values, int lo, int hi) { + // stop sorting when left/right pointers are within the same node + // because queryItems just searches through them all sequentially + if (lo / nodeCapacity < hi / nodeCapacity) { + int pivot = hoarePartition(values, lo, hi); + quickSortItemsIntoNodes(values, lo, pivot); + quickSortItemsIntoNodes(values, pivot + 1, hi); } + } - @Override - public int compare(Item item1, Item item2) { - int hcode1 = encoder.encode(item1.getEnvelope()); - int hcode2 = encoder.encode(item2.getEnvelope()); - return Integer.compare(hcode1, hcode2); + private int hoarePartition(int[] values, int lo, int hi) { + int pivot = values[(lo + hi) >> 1]; + int i = lo - 1; + int j = hi + 1; + + while (true) { + do i++; while (values[i] < pivot); + do j--; while (values[j] > pivot); + if (i >= j) return j; + swapItems(values, i, j); } } + private void swapItems(int[] values, int i, int j) { + Item tmpItemp = itemsToLoad.get(i); + itemsToLoad.set(i, itemsToLoad.get(j)); + itemsToLoad.set(j, tmpItemp); + + int tmpValue = values[i]; + values[i] = values[j]; + values[j] = tmpValue; + } + } diff --git a/modules/core/src/main/java/org/locationtech/jts/noding/MCIndexNoder.java b/modules/core/src/main/java/org/locationtech/jts/noding/MCIndexNoder.java index 9536abc3d5..399efda0d9 100644 --- a/modules/core/src/main/java/org/locationtech/jts/noding/MCIndexNoder.java +++ b/modules/core/src/main/java/org/locationtech/jts/noding/MCIndexNoder.java @@ -21,14 +21,14 @@ import org.locationtech.jts.index.chain.MonotoneChain; import org.locationtech.jts.index.chain.MonotoneChainBuilder; import org.locationtech.jts.index.chain.MonotoneChainOverlapAction; -import org.locationtech.jts.index.strtree.STRtree; +import org.locationtech.jts.index.hprtree.HPRtree; /** * Nodes a set of {@link SegmentString}s using a index based * on {@link MonotoneChain}s and a {@link SpatialIndex}. * The {@link SpatialIndex} used should be something that supports * envelope (range) queries efficiently (such as a Quadtree} - * or {@link STRtree} (which is the default index provided). + * or {@link HPRtree} (which is the default index provided). *

* The noder supports using an overlap tolerance distance . * This allows determining segment intersection using a buffer for uses @@ -40,7 +40,7 @@ public class MCIndexNoder extends SinglePassNoder { private List monoChains = new ArrayList(); - private SpatialIndex index= new STRtree(); + private SpatialIndex index= new HPRtree(); private int idCounter = 0; private Collection nodedSegStrings; // statistics diff --git a/modules/core/src/main/java/org/locationtech/jts/noding/snapround/MCIndexPointSnapper.java b/modules/core/src/main/java/org/locationtech/jts/noding/snapround/MCIndexPointSnapper.java index 043ac92640..e4c6f95cdb 100644 --- a/modules/core/src/main/java/org/locationtech/jts/noding/snapround/MCIndexPointSnapper.java +++ b/modules/core/src/main/java/org/locationtech/jts/noding/snapround/MCIndexPointSnapper.java @@ -18,7 +18,6 @@ import org.locationtech.jts.index.SpatialIndex; import org.locationtech.jts.index.chain.MonotoneChain; import org.locationtech.jts.index.chain.MonotoneChainSelectAction; -import org.locationtech.jts.index.strtree.STRtree; import org.locationtech.jts.noding.NodedSegmentString; import org.locationtech.jts.noding.SegmentString; @@ -32,10 +31,10 @@ public class MCIndexPointSnapper { //public static final int nSnaps = 0; - private STRtree index; + private SpatialIndex index; public MCIndexPointSnapper(SpatialIndex index) { - this.index = (STRtree) index; + this.index = index; } /** diff --git a/modules/core/src/test/java/test/jts/perf/index/FlatbushPerfTest.java b/modules/core/src/test/java/test/jts/perf/index/FlatbushPerfTest.java new file mode 100644 index 0000000000..989208119f --- /dev/null +++ b/modules/core/src/test/java/test/jts/perf/index/FlatbushPerfTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2019 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package test.jts.perf.index; + +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.index.SpatialIndex; +import org.locationtech.jts.index.hprtree.HPRtree; +import org.locationtech.jts.index.strtree.STRtree; +import org.locationtech.jts.util.Stopwatch; +import test.jts.perf.PerformanceTestCase; +import test.jts.perf.PerformanceTestRunner; + +import java.util.Random; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Reproduce the performance benchmark scenario that + * Flatbush + * uses, and run against spatial indexes. + */ +public class FlatbushPerfTest extends PerformanceTestCase { + private static final int NUM_ITEMS = 1_000_000; + private static final int NUM_QUERIES = 1_000; + private Envelope[] items; + private Envelope[] queries; + private HPRtree hprtree; + private STRtree strtree; + + public static void main(String[] args) { + PerformanceTestRunner.run(FlatbushPerfTest.class); + } + + public FlatbushPerfTest(String name) { + super(name); + setRunSize(new int[] { 1, 10, (int) (100 * Math.sqrt(0.1))}); + setRunIterations(1); + } + + private static Envelope randomBox(Random random, double boxSize) { + double x = random.nextDouble() * (100d - boxSize); + double y = random.nextDouble() * (100d - boxSize); + double x2 = x + random.nextDouble() * boxSize; + double y2 = y + random.nextDouble() * boxSize; + return new Envelope(x, x2, y, y2); + } + + public void setUp() + { + Random random = new Random(0); + items = new Envelope[NUM_ITEMS]; + + for (int i = 0; i < NUM_ITEMS; i++) { + items[i] = randomBox(random, 1); + } + + // warmup the jvm by building once and running queries + warmupQueries(createIndex(HPRtree::new, HPRtree::build)); + warmupQueries(createIndex(STRtree::new, STRtree::build)); + + Stopwatch sw = new Stopwatch(); + hprtree = createIndex(HPRtree::new, HPRtree::build); + System.out.println("HPRTree Build time = " + sw.getTimeString()); + + sw = new Stopwatch(); + strtree = createIndex(STRtree::new, STRtree::build); + System.out.println("STRTree Build time = " + sw.getTimeString()); + } + + private T createIndex(Supplier supplier, Consumer builder) { + T index = supplier.get(); + for (Envelope env : items) { + index.insert(env, env); + } + builder.accept(index); + return index; + } + + private void warmupQueries(SpatialIndex index) { + Random random = new Random(0); + CountItemVisitor visitor = new CountItemVisitor(); + for (int i = 0; i < NUM_QUERIES; i++) { + index.query(randomBox(random, 1), visitor); + } + } + + public void startRun(int size) + { + System.out.println("----- Query size: " + size); + Random random = new Random(0); + queries = new Envelope[NUM_QUERIES]; + for (int i = 0; i < NUM_QUERIES; i++) { + queries[i] = randomBox(random, size); + } + } + + public void runQueriesHPR() { + CountItemVisitor visitor = new CountItemVisitor(); + for (Envelope box : queries) { + hprtree.query(box, visitor); + } + System.out.println("HPRTree query result items = " + visitor.count); + } + + public void runQueriesSTR() { + CountItemVisitor visitor = new CountItemVisitor(); + for (Envelope box : queries) { + strtree.query(box, visitor); + } + System.out.println("STRTree query result items = " + visitor.count); + } +} From 1df59547d1b72ec156079938eb7adc9e635ce5b2 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 2 Jan 2024 11:57:12 -0800 Subject: [PATCH 056/123] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index bddc1b9cf7..5897fa7206 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -73,6 +73,8 @@ Distributions for older JTS versions can be obtained at the * Improve `Polygonizer` performance in some cases with many islands (#906) * Improve Convex Hull performance by avoiding duplicate uniquing (#985) +* Improve `HPRtree` performance (#1012) +* Improve performance of noding and overlay via `HPRtree` (#1012) # Version 1.19 From 39fa4a722c6aab02754860dcf7c34a873cb12b40 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 10 Jan 2024 10:35:32 -0800 Subject: [PATCH 057/123] Remove unneeded code from GeometryGraph --- .../jts/geomgraph/GeometryGraph.java | 6 +- .../testxml/general/TestRelatePA.xml | 77 ++++++++++++++++++- 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/geomgraph/GeometryGraph.java b/modules/core/src/main/java/org/locationtech/jts/geomgraph/GeometryGraph.java index 4292399a88..0d6809a286 100644 --- a/modules/core/src/main/java/org/locationtech/jts/geomgraph/GeometryGraph.java +++ b/modules/core/src/main/java/org/locationtech/jts/geomgraph/GeometryGraph.java @@ -221,12 +221,10 @@ private void add(Geometry g) private void addCollection(GeometryCollection gc) { for (int i = 0; i < gc.getNumGeometries(); i++) { - Geometry g = gc.getGeometryN(i); - if (! g.isEmpty()) { - add(g); - } + add(gc.getGeometryN(i)); } } + /** * Add a Point to the graph. */ diff --git a/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml b/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml index 07ceb7e933..4c2345a7d2 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml @@ -157,7 +157,7 @@ - mPA - empty MultiPoint element + mPA - empty MultiPoint element for A MULTIPOINT(EMPTY,(0 0)) @@ -181,6 +181,81 @@ true + + mPA - empty MultiPoint element for A, on boundary of B + + MULTIPOINT(EMPTY,(1 0)) + + + POLYGON ((1 0,0 1,-1 0,0 -1, 1 0)) + + + + true + + + false + true + false + false + false + false + true + false + true + false + + + + mPA - empty MultiPoint element for B + + POLYGON ((1 0,0 1,-1 0,0 -1, 1 0)) + + + MULTIPOINT(EMPTY,(0 0)) + + + + true + + + true + false + true + false + false + false + true + false + false + false + + + + mPA - empty MultiPoint element for B, on boundary of A + + POLYGON ((1 0,0 1,-1 0,0 -1, 1 0)) + + + MULTIPOINT(EMPTY,(1 0)) + + + + true + + + false + false + true + false + false + false + true + false + true + false + + PmA - empty MultiPolygon element From 872bc44fa38d038e5d8c9d77068b28a342b7f208 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 11 Jan 2024 13:07:56 -0800 Subject: [PATCH 058/123] Fix OffsetCurve to handle end seg issue (#1029) --- .../function/OffsetCurveFunctions.java | 22 +++++++ .../jts/operation/buffer/OffsetCurve.java | 66 ++++++++++++++----- .../jts/operation/buffer/OffsetCurveTest.java | 40 ++++++++++- 3 files changed, 108 insertions(+), 20 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/OffsetCurveFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/OffsetCurveFunctions.java index 008784e6cc..1302a5e6dc 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/OffsetCurveFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/OffsetCurveFunctions.java @@ -16,6 +16,7 @@ import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.util.GeometryCombiner; +import org.locationtech.jts.operation.buffer.BufferParameters; import org.locationtech.jts.operation.buffer.OffsetCurve; import org.locationtech.jtstest.geomfunction.Metadata; @@ -75,4 +76,25 @@ public static Geometry rawCurve(Geometry geom, double distance) return curve; } + public static Geometry rawCurveWithParams(Geometry geom, + Double distance, + @Metadata(title="Quadrant Segs") + Integer quadrantSegments, + @Metadata(title="NOT USED") + Integer capStyle, + @Metadata(title="Join style") + Integer joinStyle, + @Metadata(title="Mitre limit") + Double mitreLimit) + { + BufferParameters bufferParams = new BufferParameters(); + if (quadrantSegments >= 0) bufferParams.setQuadrantSegments(quadrantSegments); + if (joinStyle >= 0) bufferParams.setJoinStyle(joinStyle); + if (mitreLimit >= 0) bufferParams.setMitreLimit(mitreLimit); + Coordinate[] pts = OffsetCurve.rawOffset((LineString) geom, distance, bufferParams); + Geometry curve = geom.getFactory().createLineString(pts); + return curve; + } + + } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java index 79c598af06..25d044cccd 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java @@ -24,6 +24,7 @@ import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.LinearRing; import org.locationtech.jts.geom.MultiLineString; +import org.locationtech.jts.geom.MultiPoint; import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.Polygon; import org.locationtech.jts.geom.util.GeometryMapper; @@ -421,8 +422,9 @@ private int matchSegments(Coordinate raw0, Coordinate raw1, int rawCurveIndex, private static class MatchCurveSegmentAction extends MonotoneChainSelectAction { - private Coordinate p0; - private Coordinate p1; + private Coordinate raw0; + private Coordinate raw1; + private double rawLen; private int rawCurveIndex; private Coordinate[] bufferRingPts; private double matchDistance; @@ -430,11 +432,12 @@ private static class MatchCurveSegmentAction private double minRawLocation = -1; private int bufferRingMinIndex = -1; - public MatchCurveSegmentAction(Coordinate p0, Coordinate p1, + public MatchCurveSegmentAction(Coordinate raw0, Coordinate raw1, int rawCurveIndex, double matchDistance, Coordinate[] bufferRingPts, double[] rawCurveLoc) { - this.p0 = p0; - this.p1 = p1; + this.raw0 = raw0; + this.raw1 = raw1; + rawLen = raw0.distance(raw1); this.rawCurveIndex = rawCurveIndex; this.bufferRingPts = bufferRingPts; this.matchDistance = matchDistance; @@ -448,34 +451,61 @@ public int getBufferMinIndex() { public void select(MonotoneChain mc, int segIndex) { /** - * A curveRingPt segment may match all or only a portion of a single raw segment. - * There may be multiple curve ring segs that match along the raw segment. + * Generally buffer segments are no longer than raw curve segments, + * since the final buffer line likely has node points added. + * So a buffer segment may match all or only a portion of a single raw segment. + * There may be multiple buffer ring segs that match along the raw segment. + * + * HOWEVER, in some cases the buffer construction may contain + * a matching buffer segment which is slightly longer than a raw curve segment. + * Specifically, at the endpoint of a closed line with nearly parallel end segments + * - the closing fillet line is very short so is heuristically removed in the buffer. + * In this case, the buffer segment must still be matched. + * This produces closed offset curves, which is technically + * an anomaly, but only happens in rare cases. */ double frac = segmentMatchFrac(bufferRingPts[segIndex], bufferRingPts[segIndex+1], - p0, p1, matchDistance); + raw0, raw1, matchDistance); //-- no match if (frac < 0) return; //-- location is used to sort segments along raw curve double location = rawCurveIndex + frac; rawCurveLoc[segIndex] = location; - //-- record lowest index + //-- buffer seg index at lowest raw location is the curve start if (minRawLocation < 0 || location < minRawLocation) { minRawLocation = location; bufferRingMinIndex = segIndex; } } - } - private static double segmentMatchFrac(Coordinate p0, Coordinate p1, - Coordinate seg0, Coordinate seg1, double matchDistance) { - if (matchDistance < Distance.pointToSegment(p0, seg0, seg1)) + private double segmentMatchFrac(Coordinate buf0, Coordinate buf1, + Coordinate raw0, Coordinate raw1, double matchDistance) { + if (! isMatch(buf0, buf1, raw0, raw1, matchDistance)) return -1; - if (matchDistance < Distance.pointToSegment(p1, seg0, seg1)) - return -1; - //-- matched - determine position as fraction along segment - LineSegment seg = new LineSegment(seg0, seg1); - return seg.segmentFraction(p0); + + //-- matched - determine location as fraction along raw segment + LineSegment seg = new LineSegment(raw0, raw1); + return seg.segmentFraction(buf0); + } + + private boolean isMatch(Coordinate buf0, Coordinate buf1, Coordinate raw0, Coordinate raw1, double matchDistance) { + double bufSegLen = buf0.distance(buf1); + if (rawLen <= bufSegLen) { + if (matchDistance < Distance.pointToSegment(raw0, buf0, buf1)) + return false; + if (matchDistance < Distance.pointToSegment(raw1, buf0, buf1)) + return false; + } + else { + //TODO: only match longer buf segs at raw curve end segs? + if (matchDistance < Distance.pointToSegment(buf0, raw0, raw1)) + return false; + if (matchDistance < Distance.pointToSegment(buf1, raw0, raw1)) + return false; + } + return true; + } } /** diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/OffsetCurveTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/OffsetCurveTest.java index f4cc9bb177..366737150e 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/OffsetCurveTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/OffsetCurveTest.java @@ -215,14 +215,14 @@ public void testClosedCurve() { public void testOverlapTriangleInside() { checkOffsetCurve( - "LINESTRING (70 80, 10 80, 50 10, 90 80, 40 80))", 10, + "LINESTRING (70 80, 10 80, 50 10, 90 80, 40 80)", 10, "LINESTRING (70 70, 40 70, 27.23 70, 50 30.15, 72.76 70, 70 70)" ); } public void testOverlapTriangleOutside() { checkOffsetCurve( - "LINESTRING (70 80, 10 80, 50 10, 90 80, 40 80))", -10, + "LINESTRING (70 80, 10 80, 50 10, 90 80, 40 80)", -10, "LINESTRING (70 90, 40 90, 10 90, 8.11 89.82, 6.29 89.29, 4.6 88.42, 3.11 87.25, 1.87 85.82, 0.91 84.18, 0.29 82.39, 0.01 80.51, 0.1 78.61, 0.54 76.77, 1.32 75.04, 41.32 5.04, 42.42 3.48, 43.8 2.16, 45.4 1.12, 47.17 0.41, 49.05 0.05, 50.95 0.05, 52.83 0.41, 54.6 1.12, 56.2 2.16, 57.58 3.48, 58.68 5.04, 98.68 75.04, 99.46 76.77, 99.9 78.61, 99.99 80.51, 99.71 82.39, 99.09 84.18, 98.13 85.82, 96.89 87.25, 95.4 88.42, 93.71 89.29, 91.89 89.82, 90 90, 70 90)" ); } @@ -293,6 +293,15 @@ public void testInfiniteLoop() { ); } + // see https://github.com/shapely/shapely/issues/820 + public void testOffsetError() { + checkOffsetCurve( + "LINESTRING (12 20, 60 68, 111 114, 151 159, 210 218)", + 3, + "LINESTRING (9.878679656440358 22.121320343559642, 57.878679656440355 70.12132034355965, 57.99069368916718 70.22770917070595, 108.86775926900314 116.11682714467565, 148.75777204394902 160.99309151648976, 148.87867965644037 161.12132034355963, 207.87867965644037 220.12132034355963)" + ); + } + //--------------------------------------- public void testQuadSegs() { @@ -337,6 +346,33 @@ public void testMinQuadrantSegments_QGIS() { ); } + // See https://trac.osgeo.org/postgis/ticket/4072 + public void testMitreJoinError() { + checkOffsetCurve( + "LINESTRING(362194.505 5649993.044,362197.451 5649994.125,362194.624 5650001.876,362189.684 5650000.114,362192.542 5649992.324,362194.505 5649993.044)", + -0.045, 0, BufferParameters.JOIN_MITRE, -1, + "LINESTRING (362194.52050157124 5649993.001754275, 362197.5086649931 5649994.098225646, 362194.65096611937 5650001.933395073, 362189.626113625 5650000.141129872, 362192.51525161567 5649992.266257602, 362194.5204958858 5649993.001752188)" + ); + } + + // See https://trac.osgeo.org/postgis/ticket/4072 + public void testMitreJoinErrorSimple() { + checkOffsetCurve( + "LINESTRING (4.821 0.72, 7.767 1.801, 4.94 9.552, 0 7.79, 2.858 0, 4.821 0.72)", + -0.045, 0, BufferParameters.JOIN_MITRE, -1, + "LINESTRING (4.83650157122754 0.6777542748970088, 7.824664993161384 1.7742256459460533, 4.966966119329371 9.6093950732796, -0.057886375241824 7.817129871774653, 2.8312516154153906 -0.0577423980712891, 4.836495885800319 0.6777521891305186)" + ); + } + + // See https://trac.osgeo.org/postgis/ticket/3279 + public void testMitreJoinSingleLine() { + checkOffsetCurve( + "LINESTRING (0.39 -0.02, 0.4650008997915482 -0.02, 0.4667128891457749 -0.0202500016082272, 0.4683515425280024 -0.0210000000000019, 0.4699159706879993 -0.0222499999999996, 0.4714061701120011 -0.0240000000000018, 0.4929087886040002 -0.0535958153351002, 0.4968358395870001 -0.0507426457862002, 0.4774061701119963 -0.0239999999999952, 0.476353470688 -0.0222500000000011, 0.4761015425280001 -0.0210000000000007, 0.4766503813740676 -0.0202500058185111, 0.4779990890331232 -0.02, 0.6189999999999996 -0.02, 0.619 -0.0700000000000002, 0.634 -0.0700000000000002, 0.6339999999999998 -0.02, 0.65 -0.02)", + -0.002, 0, BufferParameters.JOIN_MITRE, -1, + "LINESTRING (0.39 -0.022, 0.4648556402268155 -0.022, 0.4661407414895839 -0.0221876631893964, 0.4672953866748729 -0.022716134946407, 0.4685176359449585 -0.0236927292232623, 0.4698334593862525 -0.0252379526243584, 0.4924663251198579 -0.0563894198284619, 0.499629444080312 -0.0511851092703384, 0.479075235654203 -0.022894668402962, 0.4785370545613636 -0.022, 0.6169999999999995 -0.022, 0.617 -0.0720000000000002, 0.636 -0.0720000000000002, 0.6359999999999998 -0.022, 0.65 -0.022)" + ); + } + //======================================= private static final double EQUALS_TOL = 0.05; From 788082edcf91ad99b91c52c30523e97df90877f1 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 11 Jan 2024 13:08:45 -0800 Subject: [PATCH 059/123] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 5897fa7206..81e1b34c9d 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -68,6 +68,7 @@ Distributions for older JTS versions can be obtained at the * Fix predicates for MultiPoint with EMPTY (#1015) * Fix `InteriorPoint` for MultiLineString with EMPTY (#1023) * Fix TopologyPreservingSimplifier to prevent incorrect topology from jumping components (#1024) +* Fix OffsetCurve to ensure end segments are included (#1029) ### Performance Improvements From fa4e05b673d2ce86d0ac1c8bbeab6a89fa71e9f9 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 18 Jan 2024 17:36:52 -0800 Subject: [PATCH 060/123] Fix PointLocator with BoundaryNodeRule for lines (#1031) --- .../jts/algorithm/PointLocator.java | 8 +++---- .../jts/algorithm/PointLocatorTest.java | 22 ++++++++++++++++++- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/PointLocator.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/PointLocator.java index 0a08fad14a..31da848f5e 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/PointLocator.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/PointLocator.java @@ -165,11 +165,11 @@ private int locateOnLineString(Coordinate p, LineString l) if (! l.getEnvelopeInternal().intersects(p)) return Location.EXTERIOR; CoordinateSequence seq = l.getCoordinateSequence(); - if (! l.isClosed()) { - if (p.equals(seq.getCoordinate(0)) + if (p.equals(seq.getCoordinate(0)) || p.equals(seq.getCoordinate(seq.size() - 1)) ) { - return Location.BOUNDARY; - } + int boundaryCount = l.isClosed() ? 2 : 1; + int loc = boundaryRule.isInBoundary(boundaryCount) ? Location.BOUNDARY : Location.INTERIOR; + return loc; } if (PointLocation.isOnLine(p, seq)) { return Location.INTERIOR; diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/PointLocatorTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/PointLocatorTest.java index 6cc0a501e8..8b3036473b 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/PointLocatorTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/PointLocatorTest.java @@ -67,7 +67,17 @@ public void testPolygon() throws Exception { assertEquals(Location.INTERIOR, pointLocator.locate(new Coordinate(190, 150), polygon)); } - private void runPtLocator(int expected, Coordinate pt, String wkt) + public void testRingBoundaryNodeRule() throws Exception + { + String wkt = "LINEARRING(10 10, 10 20, 20 10, 10 10)"; + Coordinate pt = new Coordinate(10, 10); + runPtLocator(Location.INTERIOR, pt, wkt, BoundaryNodeRule.MOD2_BOUNDARY_RULE); + runPtLocator(Location.BOUNDARY, pt, wkt, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE); + runPtLocator(Location.INTERIOR, pt, wkt, BoundaryNodeRule.MONOVALENT_ENDPOINT_BOUNDARY_RULE); + runPtLocator(Location.BOUNDARY, pt, wkt, BoundaryNodeRule.MULTIVALENT_ENDPOINT_BOUNDARY_RULE); + } + + private void runPtLocator(int expected, Coordinate pt, String wkt) throws Exception { Geometry geom = reader.read(wkt); @@ -76,4 +86,14 @@ private void runPtLocator(int expected, Coordinate pt, String wkt) assertEquals(expected, loc); } + private void runPtLocator(int expected, Coordinate pt, String wkt, + BoundaryNodeRule bnr) + throws Exception + { + Geometry geom = reader.read(wkt); + PointLocator pointLocator = new PointLocator(bnr); + int loc = pointLocator.locate(pt, geom); + assertEquals(expected, loc); + } + } From a79038220bd44db49bf9b5a6a56b32ad7d85ca86 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 18 Jan 2024 17:37:51 -0800 Subject: [PATCH 061/123] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 81e1b34c9d..47fc5d3e0e 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -69,6 +69,7 @@ Distributions for older JTS versions can be obtained at the * Fix `InteriorPoint` for MultiLineString with EMPTY (#1023) * Fix TopologyPreservingSimplifier to prevent incorrect topology from jumping components (#1024) * Fix OffsetCurve to ensure end segments are included (#1029) +* Fix `PointLocator` to respect `BoundaryNodeRule` for single lines (#1031) ### Performance Improvements From 3467e23ee6503e199cede3be63fced189c37ed37 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Fri, 19 Jan 2024 10:28:31 -0800 Subject: [PATCH 062/123] Add TestBuilder Selection functions for prep geoms --- .../jtstest/function/SelectionFunctions.java | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionFunctions.java index c58f9aea6e..0545649fc9 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionFunctions.java @@ -16,12 +16,23 @@ import java.util.List; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.prep.PreparedGeometry; +import org.locationtech.jts.geom.prep.PreparedGeometryFactory; import org.locationtech.jts.operation.distance.IndexedFacetDistance; - - public class SelectionFunctions { + + public static Geometry intersectsPrep(Geometry a, final Geometry mask) + { + PreparedGeometry prep = PreparedGeometryFactory.prepare(mask); + return select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return prep.intersects(g); + } + }); + } + public static Geometry intersects(Geometry a, final Geometry mask) { return select(a, new GeometryPredicate() { @@ -35,7 +46,17 @@ public static Geometry covers(Geometry a, final Geometry mask) { return select(a, new GeometryPredicate() { public boolean isTrue(Geometry g) { - return g.covers(mask); + return mask.covers(g); + } + }); + } + + public static Geometry coversPrep(Geometry a, final Geometry mask) + { + PreparedGeometry prep = PreparedGeometryFactory.prepare(mask); + return select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return prep.covers(g); } }); } @@ -168,7 +189,7 @@ public boolean isTrue(Geometry g) { }); } - private static Geometry select(Geometry geom, GeometryPredicate pred) + public static Geometry select(Geometry geom, GeometryPredicate pred) { List selected = new ArrayList(); for (int i = 0; i < geom.getNumGeometries(); i++ ) { From 13135e80226061c134193bbef5f9da6ca42d60d1 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Fri, 19 Jan 2024 10:29:34 -0800 Subject: [PATCH 063/123] Add BoundaryNodeRule toString methods --- .../jts/algorithm/BoundaryNodeRule.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/BoundaryNodeRule.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/BoundaryNodeRule.java index 7770f6273a..8b20bef1b3 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/BoundaryNodeRule.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/BoundaryNodeRule.java @@ -121,6 +121,10 @@ public boolean isInBoundary(int boundaryCount) // the "Mod-2 Rule" return boundaryCount % 2 == 1; } + + public String toString() { + return "Mod2 Boundary Node Rule"; + } } /** @@ -152,6 +156,10 @@ public boolean isInBoundary(int boundaryCount) { return boundaryCount > 0; } + + public String toString() { + return "EndPoint Boundary Node Rule"; + } } /** @@ -171,6 +179,10 @@ public boolean isInBoundary(int boundaryCount) { return boundaryCount > 1; } + + public String toString() { + return "MultiValent EndPoint Boundary Node Rule"; + } } /** @@ -189,6 +201,10 @@ public boolean isInBoundary(int boundaryCount) { return boundaryCount == 1; } + + public String toString() { + return "MonoValent EndPoint Boundary Node Rule"; + } } From 08dc89e7dd791d750e19b6a150ef686e506f1641 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Fri, 19 Jan 2024 10:58:11 -0800 Subject: [PATCH 064/123] Add SegmentString default methods --- .../jts/noding/SegmentString.java | 58 +++++++++++++++++++ .../jts/noding/SegmentStringTest.java | 52 +++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 modules/core/src/test/java/org/locationtech/jts/noding/SegmentStringTest.java diff --git a/modules/core/src/main/java/org/locationtech/jts/noding/SegmentString.java b/modules/core/src/main/java/org/locationtech/jts/noding/SegmentString.java index 8860ab59b7..88d866dad0 100644 --- a/modules/core/src/main/java/org/locationtech/jts/noding/SegmentString.java +++ b/modules/core/src/main/java/org/locationtech/jts/noding/SegmentString.java @@ -36,8 +36,66 @@ public interface SegmentString */ public void setData(Object data); + /** + * Gets the number of coordinates in this segment string. + * + * @return the number of coordinates + */ public int size(); + + /** + * Gets the segment string coordinate at a given index. + * + * @param i the coordinate index + * @return the coordinate at the index + */ public Coordinate getCoordinate(int i); + + /** + * Gets the coordinates in this segment string. + * + * @return the coordinates as an array + */ public Coordinate[] getCoordinates(); + + /** + * Tests if a segment string is a closed ring. + * + * @return true if the segment string is closed + */ public boolean isClosed(); + + /** + * Gets the previous vertex in a ring from a vertex index. + * + * @param ringSS a segment string forming a ring + * @param index the vertex index + * @return the previous vertex in the ring + * + * @see #isClosed + */ + public default Coordinate prevInRing(int index) { + int prevIndex = index - 1; + if (prevIndex < 0) { + prevIndex = size() - 2; + } + return getCoordinate( prevIndex ); + } + + /** + * Gets the next vertex in a ring from a vertex index. + * + * @param ringSS a segment string forming a ring + * @param index the vertex index + * @return the next vertex in the ring + * + * @see #isClosed + */ + public default Coordinate nextInRing(int index) { + int nextIndex = index + 1; + if (nextIndex > size() - 1) { + nextIndex = 1; + } + return getCoordinate( nextIndex ); + } } diff --git a/modules/core/src/test/java/org/locationtech/jts/noding/SegmentStringTest.java b/modules/core/src/test/java/org/locationtech/jts/noding/SegmentStringTest.java new file mode 100644 index 0000000000..04df1d98ad --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/noding/SegmentStringTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.noding; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; + +import test.jts.GeometryTestCase; + +public class SegmentStringTest extends GeometryTestCase { + + public static void main(String[] args) { + junit.textui.TestRunner.run(SegmentStringTest.class); + } + + public SegmentStringTest(String name) { + super(name); + } + + public void testNextInRing() { + SegmentString ss = create("LINESTRING(0 0, 1 2, 3 1, 0 0)"); + assertTrue(ss.isClosed()); + checkEqualXY(ss.nextInRing(0), new Coordinate(1, 2)); + checkEqualXY(ss.nextInRing(1), new Coordinate(3, 1)); + checkEqualXY(ss.nextInRing(2), new Coordinate(0, 0)); + checkEqualXY(ss.nextInRing(3), new Coordinate(1, 2)); + } + + public void testPrevInRing() { + SegmentString ss = create("LINESTRING(0 0, 1 2, 3 1, 0 0)"); + assertTrue(ss.isClosed()); + checkEqualXY(ss.prevInRing(0), new Coordinate(3, 1)); + checkEqualXY(ss.prevInRing(1), new Coordinate(0, 0)); + checkEqualXY(ss.prevInRing(2), new Coordinate(1, 2)); + checkEqualXY(ss.prevInRing(3), new Coordinate(3, 1)); + } + + private SegmentString create(String wkt) { + Geometry geom = read(wkt); + return new BasicSegmentString(geom.getCoordinates(), null); + } + +} From 7b1395b552a1cc1bede96ed23cc2c3ef390fd064 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Fri, 19 Jan 2024 11:39:47 -0800 Subject: [PATCH 065/123] Enhance PolygonNodeTopology to handle colliinear segments --- .../jts/algorithm/PolygonNodeTopology.java | 76 +++++++++++++++++-- .../algorithm/PolygonNodeTopologyTest.java | 10 +++ 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/PolygonNodeTopology.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/PolygonNodeTopology.java index a898d036cf..c96777878b 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/PolygonNodeTopology.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/PolygonNodeTopology.java @@ -24,9 +24,10 @@ public class PolygonNodeTopology { /** - * Check if the segments at a node between two rings (or one ring) cross. + * Check if four segments at a node cross. + * Typically the segments lie in two different rings, or different sections of one ring. * The node is topologically valid if the rings do not cross. - * This function assumes that the segments are not collinear. + * If any segments are collinear, the test returns false. * * @param nodePt the node location * @param a0 the previous segment endpoint in a ring @@ -42,14 +43,24 @@ public static boolean isCrossing(Coordinate nodePt, Coordinate a0, Coordinate a1 aLo = a1; aHi = a0; } - /** - * Find positions of b0 and b1. - * If they are the same they do not cross the other edge - */ + /* boolean isBetween0 = isBetween(nodePt, b0, aLo, aHi); boolean isBetween1 = isBetween(nodePt, b1, aLo, aHi); return isBetween0 != isBetween1; + */ + + /** + * Find positions of b0 and b1. + * The edges cross if the positions are different. + * If any edge is collinear they are reported as not crossing + */ + int compBetween0 = compareBetween(nodePt, b0, aLo, aHi); + if (compBetween0 == 0) return false; + int compBetween1 = compareBetween(nodePt, b1, aLo, aHi); + if (compBetween1 == 0) return false; + + return compBetween0 != compBetween1; } /** @@ -99,6 +110,28 @@ private static boolean isBetween(Coordinate origin, Coordinate p, Coordinate e0, return ! isGreater1; } + /** + * Compares whether an edge p is between or outside the edges e0 and e1, + * where the edges all originate at a common origin. + * The "inside" of e0 and e1 is the arc which does not include + * the positive X-axis at the origin. + * If p is collinear with an edge 0 is returned. + * + * @param origin the origin + * @param p the destination point of edge p + * @param e0 the destination point of edge e0 + * @param e1 the destination point of edge e1 + * @return a negative integer, zero or positive integer as the vector P lies outside, collinear with, or inside the vectors E0 and E1 + */ + private static int compareBetween(Coordinate origin, Coordinate p, Coordinate e0, Coordinate e1) { + int comp0 = compareAngle(origin, p, e0); + if (comp0 == 0) return 0; + int comp1 = compareAngle(origin, p, e1); + if (comp1 == 0) return 0; + if (comp0 > 0 && comp1 < 0) return 1; + return -1; + } + /** * Tests if the angle with the origin of a vector P is greater than that of the * vector Q. @@ -126,6 +159,37 @@ private static boolean isAngleGreater(Coordinate origin, Coordinate p, Coordinat return orient == Orientation.COUNTERCLOCKWISE; } + /** + * Compares the angles of two vectors + * relative to the positive X-axis at their origin. + * + * @param origin the origin of the vectors + * @param p the endpoint of the vector P + * @param q the endpoint of the vector Q + * @return a negative integer, zero, or a positive integer as this vector P has angle less than, equal to, or greater than vector Q + */ + public static int compareAngle(Coordinate origin, Coordinate p, Coordinate q) { + int quadrantP = quadrant(origin, p); + int quadrantQ = quadrant(origin, q); + + /** + * If the vectors are in different quadrants, + * that determines the ordering + */ + if (quadrantP > quadrantQ) return 1; + if (quadrantP < quadrantQ) return -1; + + //--- vectors are in the same quadrant + // Check relative orientation of vectors + // P > Q if it is CCW of Q + int orient = Orientation.index(origin, q, p); + switch (orient) { + case Orientation.COUNTERCLOCKWISE: return 1; + case Orientation.CLOCKWISE: return -1; + default: return 0; + } + } + private static int quadrant(Coordinate origin, Coordinate p) { double dx = p.getX() - origin.getX(); double dy = p.getY() - origin.getY(); diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/PolygonNodeTopologyTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/PolygonNodeTopologyTest.java index 3677763eaa..5ee5bd6424 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/PolygonNodeTopologyTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/PolygonNodeTopologyTest.java @@ -29,6 +29,16 @@ public void testNonCrossingQuadrant4() { "LINESTRING (1000 500, 1000 1000, 1500 1000)"); } + public void testNonCrossingCollinear() { + checkNonCrossing("LINESTRING (3 1, 5 5, 9 9)", + "LINESTRING (2 1, 5 5, 9 9)"); + } + + public void testNonCrossingBothCollinear() { + checkNonCrossing("LINESTRING (3 1, 5 5, 9 9)", + "LINESTRING (3 1, 5 5, 9 9)"); + } + public void testInteriorSegment() { checkInterior("LINESTRING (5 9, 5 5, 9 5)", "LINESTRING (5 5, 0 0)"); From 34a103317ace6b02bf070bca446b78e0f357ce08 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 14 Mar 2024 10:35:04 -0700 Subject: [PATCH 066/123] Fix buffer inverted ring removal heuristic (#1038) --- .../buffer/BufferCurveSetBuilder.java | 67 ++++++++++++------- .../jts/operation/buffer/BufferTest.java | 23 +++++++ 2 files changed, 66 insertions(+), 24 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferCurveSetBuilder.java b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferCurveSetBuilder.java index ff175f36a8..11f68f17db 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferCurveSetBuilder.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferCurveSetBuilder.java @@ -24,6 +24,7 @@ import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.LineSegment; import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.LinearRing; import org.locationtech.jts.geom.Location; @@ -327,57 +328,75 @@ private void addRingSide(Coordinate[] coord, double offsetDistance, int side, in *

* See https://github.com/locationtech/jts/issues/472 * - * @param inputPts the input ring + * @param inputRing the input ring * @param distance the buffer distance - * @param curvePts the generated offset curve + * @param curveRing the generated offset curve ring * @return true if the offset curve is inverted */ - private static boolean isRingCurveInverted(Coordinate[] inputPts, double distance, Coordinate[] curvePts) { + private static boolean isRingCurveInverted(Coordinate[] inputRing, double distance, Coordinate[] curveRing) { if (distance == 0.0) return false; /** * Only proper rings can invert. */ - if (inputPts.length <= 3) return false; + if (inputRing.length <= 3) return false; /** * Heuristic based on low chance that a ring with many vertices will invert. * This low limit ensures this test is fairly efficient. */ - if (inputPts.length >= MAX_INVERTED_RING_SIZE) return false; + if (inputRing.length >= MAX_INVERTED_RING_SIZE) return false; /** * Don't check curves which are much larger than the input. * This improves performance by avoiding checking some concave inputs * (which can produce fillet arcs with many more vertices) */ - if (curvePts.length > INVERTED_CURVE_VERTEX_FACTOR * inputPts.length) return false; + if (curveRing.length > INVERTED_CURVE_VERTEX_FACTOR * inputRing.length) return false; /** - * Check if the curve vertices are all closer to the input ring - * than the buffer distance. - * If so, the curve is NOT a valid buffer curve. + * If curve contains points which are on the buffer, + * it is not inverted and can be included in the raw curves. */ - double distTol = NEARNESS_FACTOR * Math.abs(distance); - double maxDist = maxDistance(curvePts, inputPts); - boolean isCurveTooClose = maxDist < distTol; - return isCurveTooClose; + if (hasPointOnBuffer(inputRing, distance, curveRing)) + return false; + + //-- curve is inverted, so discard it + return true; } /** - * Computes the maximum distance out of a set of points to a linestring. + * Tests if there are points on the raw offset curve which may + * lie on the final buffer curve + * (i.e. they are (approximately) at the buffer distance from the input ring). + * For efficiency this only tests a limited set of points on the curve. * - * @param pts the points - * @param line the linestring vertices - * @return the maximum distance + * @param inputRing + * @param distance + * @param curveRing + * @return true if the curve contains points lying at the required buffer distance */ - private static double maxDistance(Coordinate[] pts, Coordinate[] line) { - double maxDistance = 0; - for (Coordinate p : pts) { - double dist = Distance.pointToSegmentString(p, line); - if (dist > maxDistance) { - maxDistance = dist; + private static boolean hasPointOnBuffer(Coordinate[] inputRing, double distance, Coordinate[] curveRing) { + double distTol = NEARNESS_FACTOR * Math.abs(distance); + + for (int i = 0; i < curveRing.length - 1; i++) { + Coordinate v = curveRing[i]; + + //-- check curve vertices + double dist = Distance.pointToSegmentString(v, inputRing); + if (dist > distTol) { + return true; + } + + //-- check curve segment midpoints + int iNext = (i < curveRing.length - 1) ? i + 1 : 0; + Coordinate vnext = curveRing[iNext]; + Coordinate midPt = LineSegment.midPoint(v, vnext); + + double distMid = Distance.pointToSegmentString(midPt, inputRing); + if (distMid > distTol) { + return true; } } - return maxDistance; + return false; } /** diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/BufferTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/BufferTest.java index ab59b70567..a78c9aea43 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/BufferTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/BufferTest.java @@ -575,6 +575,22 @@ public void testLineClosedNoHole() { checkBufferHasHole(wkt, 70, false); } + public void testSmallPolygonNegativeBuffer_1() { + String wkt = "MULTIPOLYGON (((833454.7163917861 6312507.405413097, 833455.3726665961 6312510.208920742, 833456.301153878 6312514.207390314, 833492.2432584754 6312537.770332065, 833493.0901320165 6312536.098774815, 833502.6580673696 6312517.561360772, 833503.9404352929 6312515.0542803425, 833454.7163917861 6312507.405413097)))"; + checkBuffer(wkt, -3.8, + "POLYGON ((833459.9671068499 6312512.066918822, 833490.7876785189 6312532.272283619, 833498.1465258132 6312517.999574621, 833459.9671068499 6312512.066918822))"); + checkBuffer(wkt, -7, + "POLYGON ((833474.0912127121 6312517.50004999, 833489.5713439264 6312527.648521655, 833493.2674441456 6312520.479822435, 833474.0912127121 6312517.50004999))"); + } + + public void testSmallPolygonNegativeBuffer_2() { + String wkt = "POLYGON ((182719.04521570954238996 224897.14115349075291306, 182807.02887436276068911 224880.64421749324537814, 182808.47314301913138479 224877.25002362736267969, 182718.38701137207681313 224740.00115247094072402, 182711.82697281913715415 224742.08599378637154587, 182717.1393717635946814 224895.61432328051887453, 182719.04521570954238996 224897.14115349075291306))"; + checkBuffer(wkt, -5, + "POLYGON ((182717 224746.99999999997, 182722.00000000003 224891.5, 182801.99999999997 224876.49999999997, 182717 224746.99999999997))"); + checkBuffer(wkt, -30, + "POLYGON ((182745.07127364463 224835.32741176756, 182745.97926048582 224861.56823147752, 182760.5070499446 224858.844270954, 182745.07127364463 224835.32741176756))"); + } + /** * See GEOS PR https://github.com/libgeos/geos/pull/978 */ @@ -623,6 +639,13 @@ private void checkBuffer(String wkt, double dist, BufferParameters param, String checkEqual(expected, result, 0.01); } + private void checkBuffer(String wkt, double dist, String wktExpected) { + Geometry geom = read(wkt); + Geometry result = BufferOp.bufferOp(geom, dist); + Geometry expected = read(wktExpected); + checkEqual(expected, result, 0.01); + } + private void checkBufferEmpty(String wkt, double dist, boolean isEmptyExpected) { Geometry a = read(wkt); Geometry result = a.buffer(dist); From 608dcd6a4cda364a7e69017a7f563009d84cc66d Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 14 Mar 2024 10:36:51 -0700 Subject: [PATCH 067/123] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 47fc5d3e0e..b1c69933da 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -70,6 +70,7 @@ Distributions for older JTS versions can be obtained at the * Fix TopologyPreservingSimplifier to prevent incorrect topology from jumping components (#1024) * Fix OffsetCurve to ensure end segments are included (#1029) * Fix `PointLocator` to respect `BoundaryNodeRule` for single lines (#1031) +* Fix `BufferOp` Inverted Ring Removal check (#1038) ### Performance Improvements From 81639c5ff9b87ebfb98b1b745b37a52abb15b9b9 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 14 Mar 2024 11:23:31 -0700 Subject: [PATCH 068/123] Change JTSOp to require -q flag for no output --- .../jtstest/cmd/CommandOptions.java | 16 ++++++---- .../locationtech/jtstest/cmd/JTSOpCmd.java | 31 +++++++++++++------ .../locationtech/jtstest/cmd/JTSOpRunner.java | 9 ++++-- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/cmd/CommandOptions.java b/modules/app/src/main/java/org/locationtech/jtstest/cmd/CommandOptions.java index 3c4c400675..887c1004f5 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/cmd/CommandOptions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/cmd/CommandOptions.java @@ -30,19 +30,23 @@ public class CommandOptions { public static final String EACHA = "eacha"; public static final String EACHB = "eachb"; public static final String ARGS = "args"; - public static final String VALIDATE = "validate"; public static final String INDEX = "index"; + public static final String TIME = "time"; + public static final String LIMIT = "limit"; + public static final String OFFSET = "offset"; + public static final String QUIET = "q"; + public static final String VALIDATE = "validate"; + public static final String WHERE = "where"; + + public static final String SOURCE_STDIN = "stdin"; - public static final String STDIN = "stdin"; public static final String FORMAT_GML = "gml"; public static final String FORMAT_WKB = "wkb"; public static final String FORMAT_TXT = "txt"; public static final String FORMAT_WKT = "wkt"; public static final String FORMAT_GEOJSON = "geojson"; public static final String FORMAT_SVG = "svg"; - public static final String TIME = "time"; - public static final String LIMIT = "limit"; - public static final String OFFSET = "offset"; - public static final String WHERE = "where"; + + } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/cmd/JTSOpCmd.java b/modules/app/src/main/java/org/locationtech/jtstest/cmd/JTSOpCmd.java index 29df546c81..31ba6a307e 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/cmd/JTSOpCmd.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/cmd/JTSOpCmd.java @@ -38,31 +38,35 @@ * *

  * --- Compute the area of a WKT geometry, output it
- * jtsop -a some-file-with-geom.wkt -f txt area 
+ * jtsop -a some-file-with-geom.wkt area 
  * 
  * --- Validate geometries from a WKT file using limit and offset
- * jtsop -a some-file-with-geom.wkt -limit 100 -offset 40 -f txt isValid 
+ * jtsop -a some-file-with-geom.wkt -limit 100 -offset 40 isValid 
  * 
  * --- Compute the unary union of a WKT geometry, output as WKB
  * jtsop -a some-file-with-geom.wkt -f wkb Overlay.unaryUnion 
  * 
  * --- Compute the union of two geometries in WKT and WKB, output as WKT
- * jtsop -a some-file-with-geom.wkt -b some-other-geom.wkb -f wkt Overlay.Union
+ * jtsop -a some-file-with-geom.wkt -b some-other-geom.wkb Overlay.Union
  * 
  * --- Compute the buffer of distance 10 of a WKT geometry, output as GeoJSON
  * jtsop -a some-file-with-geom.wkt -f geojson Buffer.buffer 10
  * 
  * --- Compute the buffer of a literal geometry, output as WKT
- * jtsop -a "POINT (10 10)" -f wkt Buffer.buffer 10
+ * jtsop -a "POINT (10 10)" Buffer.buffer 10
  * 
  * --- Compute buffers of multiple sizes
- * jtsop -a "POINT (10 10)" -f wkt Buffer.buffer 1,10,100
+ * jtsop -a "POINT (10 10)" Buffer.buffer 1,10,100
  * 
  * --- Run op for each A 
- * jtsop -a "MULTIPOINT ((10 10), (20 20))" -eacha -f wkt Buffer.buffer
+ * jtsop -a "MULTIPOINT ((10 10), (20 20))" -eacha Buffer.buffer
  * 
  * --- Output a literal geometry as GeoJSON
  * jtsop -a "POINT (10 10)" -f geojson
+ * 
+ * --- Run op but don't output result (quiet mode) 
+ * jtsop -a "MULTIPOINT ((10 10), (20 20))" -q Buffer.buffer
+
  * 
* * @author Martin Davis @@ -120,6 +124,7 @@ private static CommandLine createCmdLine() { .addOptionSpec(new OptionSpec(CommandOptions.LIMIT, 1)) .addOptionSpec(new OptionSpec(CommandOptions.OFFSET, 1)) .addOptionSpec(new OptionSpec(CommandOptions.REPEAT, 1)) + .addOptionSpec(new OptionSpec(CommandOptions.QUIET, 0)) .addOptionSpec(new OptionSpec(CommandOptions.SRID, 1)) .addOptionSpec(new OptionSpec(CommandOptions.WHERE, 2)) .addOptionSpec(new OptionSpec(CommandOptions.VALIDATE, 0)) @@ -145,6 +150,7 @@ private static CommandLine createCmdLine() { " [ -explode", " [ -srid SRID ]", " [ -f ( txt | wkt | wkb | geojson | gml | svg ) ]", + " [ -q", " [ -time ]", " [ -v, -verbose ]", " [ -help ]", @@ -167,14 +173,15 @@ private static CommandLine createCmdLine() { " -index index the B geometries", " -repeat repeat the operation N times", " -where op v output geometry where operation result matches predicate op and value.", - " Predicates ops are: eq,ne,ge,gt,le,lt", + " Predicates ops are: eq, ne, ge, gt, le, lt", " -validate validate the result of each operation", " -geomfunc specifies class providing geometry operations", " -op separator to delineate operation arguments", "===== Output options:", - " -srid Sets the SRID on output geometries", + " -srid sets the SRID on output geometries", " -explode output atomic geometries", - " -f output format to use. If omitted output is silent", + " -f output format to use. Default is txt/wkt", + " -q quiet mode - result is not output", "===== Logging options:", " -time display execution time", " -v, -verbose display information about execution", @@ -331,7 +338,9 @@ JTSOpRunner.OpParams parseArgs(String[] args) throws ParseException, ClassNotFou ? commandLine.getOptionArgAsInt(CommandOptions.OFFSET, 0) : 0; - cmdArgs.format = commandLine.getOptionArg(CommandOptions.FORMAT, 0); + cmdArgs.format = commandLine.hasOption(CommandOptions.FORMAT) + ? commandLine.getOptionArg(CommandOptions.FORMAT, 0) + : CommandOptions.FORMAT_TXT; cmdArgs.srid = commandLine.hasOption(CommandOptions.SRID) ? commandLine.getOptionArgAsInt(CommandOptions.SRID, 0) @@ -339,6 +348,8 @@ JTSOpRunner.OpParams parseArgs(String[] args) throws ParseException, ClassNotFou cmdArgs.isIndexed = commandLine.hasOption(CommandOptions.INDEX); + cmdArgs.isQuiet = commandLine.hasOption(CommandOptions.QUIET); + cmdArgs.repeat = commandLine.hasOption(CommandOptions.REPEAT) ? commandLine.getOptionArgAsInt(CommandOptions.REPEAT, 0) : 1; diff --git a/modules/app/src/main/java/org/locationtech/jtstest/cmd/JTSOpRunner.java b/modules/app/src/main/java/org/locationtech/jtstest/cmd/JTSOpRunner.java index 76723c6c3e..cf82f216a7 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/cmd/JTSOpRunner.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/cmd/JTSOpRunner.java @@ -95,7 +95,8 @@ static class OpParams { public boolean isGeomAB = false; public boolean isCollect = false; - String format = null; + public boolean isQuiet = false; + public String format = null; public Integer repeat; public boolean eachA = false; public boolean eachB = false; @@ -361,7 +362,9 @@ private Object executeFunctionOnce(Geometry geomA, GeometryFunction func, Object if (param.validate) { validate(result); } - outputResult(result, param.isExplode, param.format); + if (! param.isQuiet) { + outputResult(result, param.isExplode, param.format); + } return result; } @@ -430,7 +433,7 @@ private List readGeometry(String geomLabel, String filename, String ge if (filename == null) return null; // must be a filename - if (filename.equalsIgnoreCase(CommandOptions.STDIN)){ + if (filename.equalsIgnoreCase(CommandOptions.SOURCE_STDIN)){ return readStdin(limit, offset); } From 981c574fc15cdc787a4fa3f3b460f17773eb17e0 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 14 Mar 2024 21:24:02 -0700 Subject: [PATCH 069/123] Improve VariableBuffer segment buffer cap generation (#1041) --- .../jts/operation/buffer/VariableBuffer.java | 126 ++++++++++++------ .../operation/buffer/VariableBufferTest.java | 40 ++++-- 2 files changed, 118 insertions(+), 48 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/VariableBuffer.java b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/VariableBuffer.java index 3624295600..6874f1d2c8 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/VariableBuffer.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/VariableBuffer.java @@ -15,6 +15,7 @@ import java.util.List; import org.locationtech.jts.algorithm.Angle; +import org.locationtech.jts.algorithm.Orientation; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateList; import org.locationtech.jts.geom.Geometry; @@ -38,6 +39,8 @@ */ public class VariableBuffer { + private static final int MIN_CAP_SEG_LEN_FACTOR = 4; + /** * Creates a buffer polygon along a line with the buffer distance interpolated * between a start distance and an end distance. @@ -270,22 +273,28 @@ public Geometry getResult() { private Polygon segmentBuffer(Coordinate p0, Coordinate p1, double dist0, double dist1) { /** - * Skip polygon if both distances are zero + * Skip buffer polygon if both distances are zero */ if (dist0 <= 0 && dist1 <= 0) return null; /** - * Compute for increasing distance only, so flip if needed + * Generation algorithm requires increasing distance, so flip if needed */ if (dist0 > dist1) { - return segmentBuffer(p1, p0, dist1, dist0); + return segmentBufferOriented(p1, p0, dist1, dist0); } - - // forward tangent line + return segmentBufferOriented(p0, p1, dist0, dist1); + } + + private Polygon segmentBufferOriented(Coordinate p0, Coordinate p1, + double dist0, double dist1) { + //-- Assert: dist0 <= dist1 + + //-- forward tangent line LineSegment tangent = outerTangent(p0, dist0, p1, dist1); - // if tangent is null then compute a buffer for largest circle + //-- if tangent is null then compute a buffer for largest circle if (tangent == null) { Coordinate center = p0; double dist = dist0; @@ -296,37 +305,32 @@ private Polygon segmentBuffer(Coordinate p0, Coordinate p1, return circle(center, dist); } - Coordinate t0 = tangent.getCoordinate(0); - Coordinate t1 = tangent.getCoordinate(1); - - // reverse tangent line on other side of segment - LineSegment seg = new LineSegment(p0, p1); - Coordinate tr0 = seg.reflect(t0); - Coordinate tr1 = seg.reflect(t1); - //-- avoid numeric jitter if first distance is zero - if (dist0 == 0) - tr0 = p0.copy(); + //-- reverse tangent line on other side of segment + LineSegment tangentReflect = reflect(tangent, p0, p1, dist0); CoordinateList coords = new CoordinateList(); - coords.add(t0, false); - coords.add(t1, false); - - // end cap - addCap(p1, dist1, t1, tr1, coords); - - coords.add(tr1, false); - coords.add(tr0, false); + //-- end cap + addCap(p1, dist1, tangent.p1, tangentReflect.p1, coords); + //-- start cap + addCap(p0, dist0, tangentReflect.p0, tangent.p0, coords); - // start cap - addCap(p0, dist0, tr0, t0, coords); - - // close - coords.add(t0, false); + coords.closeRing(); Coordinate[] pts = coords.toCoordinateArray(); Polygon polygon = geomFactory.createPolygon(pts); +//System.out.println(polygon); return polygon; } + + private LineSegment reflect(LineSegment seg, Coordinate p0, Coordinate p1, double dist0) { + LineSegment line = new LineSegment(p0, p1); + Coordinate r0 = line.reflect(seg.p0); + Coordinate r1 = line.reflect(seg.p1); + //-- avoid numeric jitter if first distance is zero (second dist must be > 0) + if (dist0 == 0) + r0 = p0.copy(); + return new LineSegment(r0, r1); + } /** * Returns a circular polygon. @@ -350,6 +354,10 @@ private Polygon circle(Coordinate center, double radius) { /** * Adds a semi-circular cap CCW around the point p. + * <>p> + * The vertices in caps are generated at fixed angles around a point. + * This allows caps at the same point to share vertices, + * which reduces artifacts when the segment buffers are merged. * * @param p the centre point of the cap * @param r the cap radius @@ -358,12 +366,14 @@ private Polygon circle(Coordinate center, double radius) { * @param coords the coordinate list to add to */ private void addCap(Coordinate p, double r, Coordinate t1, Coordinate t2, CoordinateList coords) { - //-- handle zero-width at vertex + //-- if radius is zero just copy the vertex if (r == 0) { coords.add(p.copy(), false); return; } + coords.add(t1, false); + double angStart = Angle.angle(p, t1); double angEnd = Angle.angle(p, t2); if (angStart < angEnd) @@ -372,18 +382,55 @@ private void addCap(Coordinate p, double r, Coordinate t1, Coordinate t2, Coordi int indexStart = capAngleIndex(angStart); int indexEnd = capAngleIndex(angEnd); - for (int i = indexStart; i > indexEnd; i--) { - // use negative increment to create points CW + double capSegLen = r * 2 * Math.sin(Math.PI / 4 / quadrantSegs); + double minSegLen = capSegLen / MIN_CAP_SEG_LEN_FACTOR; + + for (int i = indexStart; i >= indexEnd; i--) { + //-- use negative increment to create points CW double ang = capAngle(i); - coords.add( projectPolar(p, r, ang), false ); + Coordinate capPt = projectPolar(p, r, ang); + + boolean isCapPointHighQuality = true; + /** + * Due to the fixed locations of the cap points, + * a start or end cap point might create + * a "reversed" segment to the next tangent point. + * This causes an unwanted narrow spike in the buffer curve, + * which can cause holes in the final buffer polygon. + * These checks remove these points. + */ + if (i == indexStart + && Orientation.CLOCKWISE != Orientation.index(p, t1, capPt)) { + isCapPointHighQuality = false; + } + else if (i == indexEnd + && Orientation.COUNTERCLOCKWISE != Orientation.index(p, t2, capPt)) { + isCapPointHighQuality = false; + } + + /** + * Remove short segments between the cap and the tangent segments. + */ + if (capPt.distance(t1) < minSegLen) { + isCapPointHighQuality = false; + } + else if (capPt.distance(t2) < minSegLen) { + isCapPointHighQuality = false; + } + + if (isCapPointHighQuality) { + coords.add(capPt, false ); + } } + + coords.add(t2, false); } /** - * Computes the angle for the given cap point index. + * Computes the actual angle for a cap angle index. * - * @param index the fillet angle index - * @return + * @param index the cap angle index + * @return the angle */ private double capAngle(int index) { double capSegAng = Math.PI / 2 / quadrantSegs; @@ -392,15 +439,13 @@ private double capAngle(int index) { /** * Computes the canonical cap point index for a given angle. - * The angle is rounded down to the next lower - * index. + * The angle is rounded down to the next lower index. *

* In order to reduce the number of points created by overlapping end caps, * cap points are generated at the same locations around a circle. * The index is the index of the points around the circle, * with 0 being the point at (1,0). - * The total number of points around the circle is - * 4 * quadrantSegs. + * The total number of points around the circle is 4 * quadrantSegs. * * @param ang the angle * @return the index for the angle. @@ -414,6 +459,7 @@ private int capAngleIndex(double ang) { /** * Computes the two circumference points defining the outer tangent line * between two circles. + * The tangent line may be null if one circle mostly overlaps the other. *

* For the algorithm see Wikipedia. * diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/VariableBufferTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/VariableBufferTest.java index b5e65a94e8..c0370b2f1a 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/VariableBufferTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/VariableBufferTest.java @@ -43,52 +43,76 @@ public void testZeroLength() { public void testSegmentInverseDist() { checkBuffer("LINESTRING (100 100, 200 100)", 10, 1, - "POLYGON ((200.09 99.00405823463417, 100.9 90.04058234634172, 100 90, 98.04909677983872 90.19214719596769, 96.1731656763491 90.76120467488714, 94.44429766980397 91.68530387697454, 92.92893218813452 92.92893218813452, 91.68530387697454 94.44429766980397, 90.76120467488713 96.1731656763491, 90.19214719596769 98.04909677983872, 90 100, 90.19214719596769 101.95090322016128, 90.76120467488713 103.8268343236509, 91.68530387697454 105.55570233019603, 92.92893218813452 107.07106781186548, 94.44429766980397 108.31469612302546, 96.1731656763491 109.23879532511287, 98.04909677983872 109.80785280403231, 100 110, 100.9 109.95941765365829, 200.09 100.99594176536583, 200.19509032201614 100.98078528040323, 200.3826834323651 100.9238795325113, 200.5555702330196 100.83146961230254, 200.70710678118655 100.70710678118655, 200.83146961230256 100.55557023301961, 200.92387953251128 100.38268343236508, 200.98078528040324 100.19509032201613, 201 100, 200.98078528040324 99.80490967798387, 200.92387953251128 99.61731656763492, 200.83146961230256 99.44442976698039, 200.70710678118655 99.29289321881345, 200.5555702330196 99.16853038769746, 200.3826834323651 99.0761204674887, 200.09 99.00405823463417))" + "POLYGON ((100 90, 98.05 90.19, 96.17 90.76, 94.44 91.69, 92.93 92.93, 91.69 94.44, 90.76 96.17, 90.19 98.05, 90 100, 90.19 101.95, 90.76 103.83, 91.69 105.56, 92.93 107.07, 94.44 108.31, 96.17 109.24, 98.05 109.81, 100 110, 100.9 109.96, 200.09 101, 200.2 100.98, 200.38 100.92, 200.56 100.83, 200.71 100.71, 200.83 100.56, 200.92 100.38, 200.98 100.2, 201 100, 200.98 99.8, 200.92 99.62, 200.83 99.44, 200.71 99.29, 200.56 99.17, 200.38 99.08, 200.2 99.02, 200.09 99, 100.9 90.04, 100 90))" ); } public void testSegmentSameDist() { checkBuffer("LINESTRING (100 100, 200 100)", 10, 10, - "POLYGON ((90.192 101.951, 90.761 103.827, 91.685 105.556, 92.929 107.071, 94.444 108.315, 96.173 109.239, 98.049 109.808, 100 110, 200 110, 201.951 109.808, 203.827 109.239, 205.556 108.315, 207.071 107.071, 208.315 105.556, 209.239 103.827, 209.808 101.951, 210 100, 209.808 98.049, 209.239 96.173, 208.315 94.444, 207.071 92.929, 205.556 91.685, 203.827 90.761, 201.951 90.192, 200 90, 100 90, 98.049 90.192, 96.173 90.761, 94.444 91.685, 92.929 92.929, 91.685 94.444, 90.761 96.173, 90.192 98.049, 90 100, 90.192 101.951))" + "POLYGON ((201.95 109.81, 203.83 109.24, 205.56 108.31, 207.07 107.07, 208.31 105.56, 209.24 103.83, 209.81 101.95, 210 100, 209.81 98.05, 209.24 96.17, 208.31 94.44, 207.07 92.93, 205.56 91.69, 203.83 90.76, 201.95 90.19, 200 90, 100 90, 98.05 90.19, 96.17 90.76, 94.44 91.69, 92.93 92.93, 91.69 94.44, 90.76 96.17, 90.19 98.05, 90 100, 90.19 101.95, 90.76 103.83, 91.69 105.56, 92.93 107.07, 94.44 108.31, 96.17 109.24, 98.05 109.81, 100 110, 200 110, 201.95 109.81))" ); } public void testOneSegment() { checkBuffer("LINESTRING (100 100, 200 100)", 10, 30, -"POLYGON ((98 109.79795897113272, 194 129.39387691339815, 194.14729033951616 129.42355841209692, 200 130, 205.85270966048384 129.42355841209692, 211.4805029709527 127.7163859753386, 216.66710699058808 124.94408836907635, 221.21320343559643 121.21320343559643, 224.94408836907635 116.66710699058807, 227.7163859753386 111.4805029709527, 229.42355841209692 105.85270966048385, 230 100, 229.42355841209692 94.14729033951615, 227.7163859753386 88.5194970290473, 224.94408836907635 83.33289300941193, 221.21320343559643 78.78679656440357, 216.66710699058808 75.05591163092365, 211.4805029709527 72.2836140246614, 205.85270966048384 70.57644158790309, 200 70, 194 70.60612308660184, 98 90.20204102886728, 96.1731656763491 90.76120467488714, 94.44429766980397 91.68530387697454, 92.92893218813452 92.92893218813452, 91.68530387697454 94.44429766980397, 90.76120467488713 96.1731656763491, 90.19214719596769 98.04909677983872, 90 100, 90.19214719596769 101.95090322016128, 90.76120467488713 103.8268343236509, 91.68530387697454 105.55570233019603, 92.92893218813452 107.07106781186548, 94.44429766980397 108.31469612302546, 96.1731656763491 109.23879532511287, 98 109.79795897113272))" +"POLYGON ((200 130, 205.85 129.42, 211.48 127.72, 216.67 124.94, 221.21 121.21, 224.94 116.67, 227.72 111.48, 229.42 105.85, 230 100, 229.42 94.15, 227.72 88.52, 224.94 83.33, 221.21 78.79, 216.67 75.06, 211.48 72.28, 205.85 70.58, 200 70, 194 70.61, 98 90.2, 96.17 90.76, 94.44 91.69, 92.93 92.93, 91.69 94.44, 90.76 96.17, 90.19 98.05, 90 100, 90.19 101.95, 90.76 103.83, 91.69 105.56, 92.93 107.07, 94.44 108.31, 96.17 109.24, 98 109.8, 194 129.39, 200 130))" ); } public void testSegments2() { checkBuffer("LINESTRING( 0 0, 40 40, 60 -20)", 10, 20, - "POLYGON ((53.52863576494982 45.80469132164433, 78.37960104024995 -12.113919503248614, 78.47759065022574 -12.346331352698204, 79.61570560806462 -16.098193559677433, 80 -20, 79.61570560806462 -23.901806440322567, 78.47759065022574 -27.653668647301796, 76.62939224605091 -31.111404660392044, 74.14213562373095 -34.14213562373095, 71.11140466039204 -36.629392246050905, 67.6536686473018 -38.477590650225736, 63.90180644032257 -39.61570560806461, 60 -40, 56.09819355967743 -39.61570560806461, 52.34633135269821 -38.477590650225736, 48.88859533960796 -36.629392246050905, 45.85786437626905 -34.14213562373095, 43.370607753949095 -31.111404660392044, 40.56467086974921 -24.718896226748868, 31.314401806419635 13.379424895487343, 6.456226258387812 -7.636566145886759, 5.555702330196018 -8.314696123025454, 3.8268343236509 -9.238795325112866, 1.950903220161283 -9.807852804032304, 0 -10, -1.9509032201612866 -9.807852804032303, -3.8268343236509033 -9.238795325112864, -5.555702330196022 -8.314696123025453, -7.071067811865477 -7.071067811865475, -8.314696123025454 -5.55570233019602, -9.238795325112868 -3.8268343236508966, -9.807852804032304 -1.9509032201612837, -10 0, -9.807852804032304 1.9509032201612861, -9.238795325112868 3.826834323650899, -8.314696123025453 5.555702330196022, -7.636566145886759 6.456226258387811, 28.75793640390754 49.5044428085851, 29.59042683391263 50.40957316608737, 31.821250844443494 52.240363117601376, 34.366379598327015 53.60076277898068, 37.12800522487612 54.4384927541594, 40 54.721359549995796, 42.87199477512389 54.4384927541594, 45.633620401672985 53.60076277898068, 48.17874915555651 52.240363117601376, 50.40957316608737 50.40957316608737, 52.240363117601376 48.17874915555651, 53.52863576494982 45.80469132164433))" + "POLYGON ((79.62 -16.1, 80 -20, 79.62 -23.9, 78.48 -27.65, 76.63 -31.11, 74.14 -34.14, 71.11 -36.63, 67.65 -38.48, 63.9 -39.62, 60 -40, 56.1 -39.62, 52.35 -38.48, 48.89 -36.63, 45.86 -34.14, 43.37 -31.11, 41.52 -27.65, 40.56 -24.72, 31.31 13.38, 6.46 -7.64, 5.56 -8.31, 3.83 -9.24, 1.95 -9.81, 0 -10, -1.95 -9.81, -3.83 -9.24, -5.56 -8.31, -7.07 -7.07, -8.31 -5.56, -9.24 -3.83, -9.81 -1.95, -10 0, -9.81 1.95, -9.24 3.83, -8.31 5.56, -7.64 6.46, 28.76 49.5, 29.59 50.41, 31.82 52.24, 34.37 53.6, 37.13 54.44, 40 54.72, 42.87 54.44, 45.63 53.6, 48.18 52.24, 50.41 50.41, 52.24 48.18, 53.53 45.8, 78.38 -12.11, 79.62 -16.1))" ); } public void testLargeDistance() { checkBuffer("LINESTRING( 0 0, 10 10)", 1, 200, - "POLYGON ((-190 10, -186.1570560806461 49.01806440322572, -174.77590650225736 86.53668647301798, -156.29392246050907 121.11404660392043, -131.42135623730948 151.4213562373095, -101.11404660392039 176.29392246050907, -66.53668647301795 194.77590650225736, -29.018064403225637 206.1570560806461, 10 210, 49.018064403225665 206.1570560806461, 86.53668647301797 194.77590650225736, 121.11404660392046 176.29392246050904, 151.4213562373095 151.42135623730948, 176.29392246050904 121.11404660392043, 194.77590650225736 86.53668647301795, 206.1570560806461 49.01806440322565, 210 10, 206.15705608064607 -29.018064403225743, 194.7759065022573 -66.53668647301808, 176.29392246050904 -101.11404660392043, 151.42135623730948 -131.42135623730954, 121.11404660392037 -156.2939224605091, 86.536686473018 -174.77590650225733, 49.01806440322566 -186.1570560806461, 10 -190, -29.018064403225736 -186.15705608064607, -66.53668647301807 -174.7759065022573, -101.11404660392043 -156.29392246050904, -131.42135623730954 -131.42135623730948, -156.2939224605091 -101.11404660392039, -174.77590650225736 -66.53668647301794, -186.1570560806461 -29.018064403225672, -190 10))" + "POLYGON ((206.16 -29.02, 194.78 -66.54, 176.29 -101.11, 151.42 -131.42, 121.11 -156.29, 86.54 -174.78, 49.02 -186.16, 10 -190, -29.02 -186.16, -66.54 -174.78, -101.11 -156.29, -131.42 -131.42, -156.29 -101.11, -174.78 -66.54, -186.16 -29.02, -190 10, -186.16 49.02, -174.78 86.54, -156.29 121.11, -131.42 151.42, -101.11 176.29, -66.54 194.78, -29.02 206.16, 10 210, 49.02 206.16, 86.54 194.78, 121.11 176.29, 151.42 151.42, 176.29 121.11, 194.78 86.54, 206.16 49.02, 210 10, 206.16 -29.02))" ); } public void testZeroDistanceAtVertex() { checkBuffer("LINESTRING( 10 10, 20 20, 30 30)", new double[] { 5, 0, 5 }, - "MULTIPOLYGON (((5.096 10.975, 5.381 11.913, 5.843 12.778, 6.464 13.536, 7.222 14.157, 7.943 14.557, 20 20, 14.557 7.943, 14.157 7.222, 13.536 6.464, 12.778 5.843, 11.913 5.381, 10.975 5.096, 10 5, 9.025 5.096, 8.087 5.381, 7.222 5.843, 6.464 6.464, 5.843 7.222, 5.381 8.087, 5.096 9.025, 5 10, 5.096 10.975)), ((25.443 32.057, 25.843 32.778, 26.464 33.536, 27.222 34.157, 28.087 34.619, 29.025 34.904, 30 35, 30.975 34.904, 31.913 34.619, 32.778 34.157, 33.536 33.536, 34.157 32.778, 34.619 31.913, 34.904 30.975, 35 30, 34.904 29.025, 34.619 28.087, 34.157 27.222, 33.536 26.464, 32.057 25.443, 20 20, 25.443 32.057)))" + "MULTIPOLYGON (((5.1 10.98, 5.38 11.91, 5.84 12.78, 6.46 13.54, 7.22 14.16, 7.94 14.56, 20 20, 14.56 7.94, 14.16 7.22, 13.54 6.46, 12.78 5.84, 11.91 5.38, 10.98 5.1, 10 5, 9.02 5.1, 8.09 5.38, 7.22 5.84, 6.46 6.46, 5.84 7.22, 5.38 8.09, 5.1 9.02, 5 10, 5.1 10.98)), ((25.44 32.06, 25.84 32.78, 26.46 33.54, 27.22 34.16, 28.09 34.62, 29.02 34.9, 30 35, 30.98 34.9, 31.91 34.62, 32.78 34.16, 33.54 33.54, 34.16 32.78, 34.62 31.91, 34.9 30.98, 35 30, 34.9 29.02, 34.62 28.09, 34.16 27.22, 33.54 26.46, 32.78 25.84, 32.06 25.44, 20 20, 25.44 32.06)))" ); } public void testZeroDistancesForSegment() { checkBuffer("LINESTRING( 10 10, 20 20, 30 30, 40 40)", new double[] { 5, 0, 0, 5 }, - "MULTIPOLYGON (((5.096 10.975, 5.381 11.913, 5.843 12.778, 6.464 13.536, 7.222 14.157, 7.943 14.557, 20 20, 14.557 7.943, 14.157 7.222, 13.536 6.464, 12.778 5.843, 11.913 5.381, 10.975 5.096, 10 5, 9.025 5.096, 8.087 5.381, 7.222 5.843, 6.464 6.464, 5.843 7.222, 5.381 8.087, 5.096 9.025, 5 10, 5.096 10.975)), ((35.443 42.057, 35.843 42.778, 36.464 43.536, 37.222 44.157, 38.087 44.619, 39.025 44.904, 40 45, 40.975 44.904, 41.913 44.619, 42.778 44.157, 43.536 43.536, 44.157 42.778, 44.619 41.913, 44.904 40.975, 45 40, 44.904 39.025, 44.619 38.087, 44.157 37.222, 43.536 36.464, 42.057 35.443, 30 30, 35.443 42.057)))" + "MULTIPOLYGON (((5.1 10.98, 5.38 11.91, 5.84 12.78, 6.46 13.54, 7.22 14.16, 7.94 14.56, 20 20, 14.56 7.94, 14.16 7.22, 13.54 6.46, 12.78 5.84, 11.91 5.38, 10.98 5.1, 10 5, 9.02 5.1, 8.09 5.38, 7.22 5.84, 6.46 6.46, 5.84 7.22, 5.38 8.09, 5.1 9.02, 5 10, 5.1 10.98)), ((35.44 42.06, 35.84 42.78, 36.46 43.54, 37.22 44.16, 38.09 44.62, 39.02 44.9, 40 45, 40.98 44.9, 41.91 44.62, 42.78 44.16, 43.54 43.54, 44.16 42.78, 44.62 41.91, 44.9 40.98, 45 40, 44.9 39.02, 44.62 38.09, 44.16 37.22, 43.54 36.46, 42.78 35.84, 42.06 35.44, 30 30, 35.44 42.06)))" ); } - + + // see https://github.com/locationtech/jts/issues/998 + public void testIssue998_Spike() { + checkBuffer("LINESTRING (0.024520295 69.50077743, 0.000508719 74.50086084, 0 76.39546845)", + new double[] { 6.47, 6.9, 7 }, + "POLYGON ((-6.87 77.76, -6.47 79.07, -5.82 80.28, -4.95 81.35, -3.89 82.22, -2.68 82.86, -1.37 83.26, 0 83.4, 1.37 83.26, 2.68 82.86, 3.89 82.22, 4.95 81.35, 5.82 80.28, 6.47 79.07, 6.87 77.76, 7 76.4, 6.99 76.03, 6.89 74.14, 6.88 74.08, 6.88 73.94, 6.47 68.98, 6.37 68.24, 6 67.02, 5.4 65.91, 4.6 64.93, 3.62 64.12, 2.5 63.52, 1.29 63.16, 0.02 63.03, -1.24 63.16, -2.45 63.52, -3.57 64.12, -4.55 64.93, -5.36 65.91, -5.95 67.02, -6.32 68.24, -6.42 68.91, -6.87 73.87, -6.88 74.05, -6.89 74.13, -6.99 76.02, -7 76.4, -6.87 77.76))" + ); + } + + public void testNoReverseSpike() { + checkBuffer("LINESTRING (0 70, 0 80)", + new double[] { 4, 7 }, + "POLYGON ((-6.87 78.63, -7 80, -6.87 81.37, -6.47 82.68, -5.82 83.89, -4.95 84.95, -3.89 85.82, -2.68 86.47, -1.37 86.87, 0 87, 1.37 86.87, 2.68 86.47, 3.89 85.82, 4.95 84.95, 5.82 83.89, 6.47 82.68, 6.87 81.37, 7 80, 6.87 78.63, 6.68 77.9, 3.82 68.8, 3.7 68.47, 3.33 67.78, 2.83 67.17, 2.22 66.67, 1.53 66.3, 0.78 66.08, 0 66, -0.78 66.08, -1.53 66.3, -2.22 66.67, -2.83 67.17, -3.33 67.78, -3.7 68.47, -3.82 68.8, -6.68 77.9, -6.87 78.63))" + ); + } + + public void testNoShortCapSegments() { + checkBuffer("LINESTRING (6.85 78.25, 18 87)", + new double[] { 5, 9 }, + "POLYGON ((11.64 93.36, 13 94.48, 14.56 95.31, 16.24 95.83, 18 96, 19.76 95.83, 21.44 95.31, 23 94.48, 24.36 93.36, 25.48 92, 26.31 90.44, 26.83 88.76, 27 87, 26.83 85.24, 26.31 83.56, 25.48 82, 24.36 80.64, 23 79.52, 21.33 78.64, 8.7 73.61, 7.83 73.35, 6.85 73.25, 5.87 73.35, 4.94 73.63, 4.07 74.09, 3.31 74.71, 2.69 75.47, 2.23 76.34, 1.95 77.27, 1.85 78.25, 1.95 79.23, 2.23 80.16, 2.78 81.15, 10.67 92.22, 11.64 93.36))" + ); + } + + //================================================================ + private void checkBuffer(String wkt, double startDist, double endDist, String wktExpected) { Geometry geom = read(wkt); From f568af79876a8c7a359ae10fd0ff5b9d62634a27 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 14 Mar 2024 21:25:26 -0700 Subject: [PATCH 070/123] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index b1c69933da..782bb90c2e 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -71,6 +71,7 @@ Distributions for older JTS versions can be obtained at the * Fix OffsetCurve to ensure end segments are included (#1029) * Fix `PointLocator` to respect `BoundaryNodeRule` for single lines (#1031) * Fix `BufferOp` Inverted Ring Removal check (#1038) +* Improve `VariableBuffer` segment buffer cap generation (#1041) ### Performance Improvements From 848a02832ca437a5a485860d6b3d1223403ec871 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 9 Apr 2024 10:46:23 -0700 Subject: [PATCH 071/123] Fix Conversion.toGeometryCollection for null first arg --- .../locationtech/jtstest/function/ConversionFunctions.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/ConversionFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/ConversionFunctions.java index 61915b749a..3986d0bf94 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/ConversionFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/ConversionFunctions.java @@ -73,12 +73,12 @@ public static Geometry toMultiPolygon(Geometry g1, Geometry g2) .createMultiPolygon( GeometryFactory.toPolygonArray(polys)); } - public static Geometry toGeometryCollection(Geometry g, Geometry g2) + public static Geometry toGeometryCollection(Geometry g1, Geometry g2) { List atomicGeoms = new ArrayList(); - if (g != null) addComponents(g, atomicGeoms); + if (g1 != null) addComponents(g1, atomicGeoms); if (g2 != null) addComponents(g2, atomicGeoms); - return g.getFactory().createGeometryCollection( + return FunctionsUtil.getFactoryOrDefault(g1, g2).createGeometryCollection( GeometryFactory.toGeometryArray(atomicGeoms)); } From 6cb096bda958a14a4e58d0214493c4be8b00b914 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 10 Apr 2024 19:45:53 -0700 Subject: [PATCH 072/123] Add PointLocation.isOnSegment function (#1048) --- .../jts/algorithm/CGAlgorithms.java | 4 +- .../jts/algorithm/PointLocation.java | 30 ++++++++++---- .../valid/PolygonTopologyAnalyzer.java | 7 +--- ...OnLineTest.java => PointLocationTest.java} | 41 ++++++++++++++----- 4 files changed, 57 insertions(+), 25 deletions(-) rename modules/core/src/test/java/org/locationtech/jts/algorithm/{PointLocationOnLineTest.java => PointLocationTest.java} (54%) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/CGAlgorithms.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/CGAlgorithms.java index 5697bd2c5a..dad9032d41 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/CGAlgorithms.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/CGAlgorithms.java @@ -179,12 +179,10 @@ public static int locatePointInRing(Coordinate p, Coordinate[] ring) */ public static boolean isOnLine(Coordinate p, Coordinate[] pt) { - LineIntersector lineIntersector = new RobustLineIntersector(); for (int i = 1; i < pt.length; i++) { Coordinate p0 = pt[i - 1]; Coordinate p1 = pt[i]; - lineIntersector.computeIntersection(p, p0, p1); - if (lineIntersector.hasIntersection()) { + if (PointLocation.isOnSegment(p, p0, p1)) { return true; } } diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/PointLocation.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/PointLocation.java index 54fa6c4351..2b694836b7 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/PointLocation.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/PointLocation.java @@ -13,17 +13,37 @@ import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Location; /** * Functions for locating points within basic geometric - * structures such as lines and rings. + * structures such as line segments, lines and rings. * * @author Martin Davis * */ public class PointLocation { + /** + * Tests whether a point lies on a line segment. + * + * @param p the point to test + * @param p0 a point of the line segment + * @param p1 a point of the line segment + * @return true if the point lies on the line segment + */ + public static boolean isOnSegment(Coordinate p, Coordinate p0, Coordinate p1) { + //-- test envelope first since it's faster + if (! Envelope.intersects(p0, p1, p)) + return false; + //-- handle zero-length segments + if (p.equals2D(p0)) + return true; + boolean isOnLine = Orientation.COLLINEAR == Orientation.index(p0, p1, p); + return isOnLine; + } + /** * Tests whether a point lies on the line defined by a list of * coordinates. @@ -35,12 +55,10 @@ public class PointLocation { */ public static boolean isOnLine(Coordinate p, Coordinate[] line) { - LineIntersector lineIntersector = new RobustLineIntersector(); for (int i = 1; i < line.length; i++) { Coordinate p0 = line[i - 1]; Coordinate p1 = line[i]; - lineIntersector.computeIntersection(p, p0, p1); - if (lineIntersector.hasIntersection()) { + if (isOnSegment(p, p0, p1)) { return true; } } @@ -58,15 +76,13 @@ public static boolean isOnLine(Coordinate p, Coordinate[] line) */ public static boolean isOnLine(Coordinate p, CoordinateSequence line) { - LineIntersector lineIntersector = new RobustLineIntersector(); Coordinate p0 = new Coordinate(); Coordinate p1 = new Coordinate(); int n = line.size(); for (int i = 1; i < n; i++) { line.getCoordinate(i-1, p0); line.getCoordinate(i, p1); - lineIntersector.computeIntersection(p, p0, p1); - if (lineIntersector.hasIntersection()) { + if (isOnSegment(p, p0, p1)) { return true; } } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/valid/PolygonTopologyAnalyzer.java b/modules/core/src/main/java/org/locationtech/jts/operation/valid/PolygonTopologyAnalyzer.java index 4e2f8d21a8..2dac531ce5 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/valid/PolygonTopologyAnalyzer.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/valid/PolygonTopologyAnalyzer.java @@ -14,16 +14,15 @@ import java.util.ArrayList; import java.util.List; -import org.locationtech.jts.algorithm.LineIntersector; import org.locationtech.jts.algorithm.Orientation; import org.locationtech.jts.algorithm.PointLocation; import org.locationtech.jts.algorithm.PolygonNodeTopology; -import org.locationtech.jts.algorithm.RobustLineIntersector; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateArrays; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.LinearRing; import org.locationtech.jts.geom.Location; +import org.locationtech.jts.geom.MultiPolygon; import org.locationtech.jts.geom.Polygon; import org.locationtech.jts.noding.BasicSegmentString; import org.locationtech.jts.noding.MCIndexNoder; @@ -185,10 +184,8 @@ private static int ringIndexNext(Coordinate[] ringPts, int index) { * @return the intersection segment index, or -1 if no intersection is found */ private static int intersectingSegIndex(Coordinate[] ringPts, Coordinate pt) { - LineIntersector li = new RobustLineIntersector(); for (int i = 0; i < ringPts.length - 1; i++) { - li.computeIntersection(pt, ringPts[i], ringPts[i + 1]); - if (li.hasIntersection()) { + if (PointLocation.isOnSegment(pt, ringPts[i], ringPts[i + 1])) { //-- check if pt is the start point of the next segment if (pt.equals2D(ringPts[i + 1])) { return i + 1; diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/PointLocationOnLineTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/PointLocationTest.java similarity index 54% rename from modules/core/src/test/java/org/locationtech/jts/algorithm/PointLocationOnLineTest.java rename to modules/core/src/test/java/org/locationtech/jts/algorithm/PointLocationTest.java index f7399b80da..216308f2b3 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/PointLocationOnLineTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/PointLocationTest.java @@ -12,34 +12,31 @@ package org.locationtech.jts.algorithm; import org.locationtech.jts.geom.Coordinate; -import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.LineString; -import org.locationtech.jts.io.ParseException; -import org.locationtech.jts.io.WKTReader; -import junit.framework.TestCase; import junit.textui.TestRunner; import test.jts.GeometryTestCase; /** - * Tests {@link PointLocation#isOnLine(Coordinate, Coordinate[])}. + * Tests {@link PointLocation}. * * @version 1.15 */ -public class PointLocationOnLineTest extends GeometryTestCase { +public class PointLocationTest extends GeometryTestCase { + public static void main(String args[]) { - TestRunner.run(PointLocationOnLineTest.class); + TestRunner.run(PointLocationTest.class); } - public PointLocationOnLineTest(String name) { + public PointLocationTest(String name) { super(name); } - public void testOnVertex() throws Exception { + public void testOnLineOnVertex() throws Exception { checkOnLine(20, 20, "LINESTRING (0 00, 20 20, 30 30)", true); } - public void testOnSegment() throws Exception { + public void testOnLineInSegment() throws Exception { checkOnLine(10, 10, "LINESTRING (0 0, 20 20, 0 40)", true); checkOnLine(10, 30, "LINESTRING (0 0, 20 20, 0 40)", true); } @@ -48,6 +45,30 @@ public void testNotOnLine() throws Exception { checkOnLine(0, 100, "LINESTRING (10 10, 20 10, 30 10)", false); } + public void testOnSegment() { + checkOnSegment(5, 5, "LINESTRING(0 0, 9 9)", true); + checkOnSegment(0, 0, "LINESTRING(0 0, 9 9)", true); + checkOnSegment(9, 9, "LINESTRING(0 0, 9 9)", true); + } + + public void testNotOnSegment() { + checkOnSegment(5, 6, "LINESTRING(0 0, 9 9)", false); + checkOnSegment(10, 10, "LINESTRING(0 0, 9 9)", false); + checkOnSegment(9, 9.00001, "LINESTRING(0 0, 9 9)", false); + } + + public void testOnZeroLengthSegment() { + checkOnSegment(1, 1, "LINESTRING(1 1, 1 1)", true); + checkOnSegment(1, 2, "LINESTRING(1 1, 1 1)", false); + } + + private void checkOnSegment(double x, double y, String wktLine, boolean expected) { + LineString line = (LineString) read(wktLine); + Coordinate p0 = line.getCoordinateN(0); + Coordinate p1 = line.getCoordinateN(1); + assertTrue(expected == PointLocation.isOnSegment(new Coordinate(x,y), p0, p1)); + } + void checkOnLine(double x, double y, String wktLine, boolean expected) { LineString line = (LineString) read(wktLine); assertTrue(expected == PointLocation.isOnLine(new Coordinate(x,y), line.getCoordinates())); From 93f0e35289246e0847a8c09505974e07c10624e2 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 10 Apr 2024 19:46:45 -0700 Subject: [PATCH 073/123] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 782bb90c2e..7f100f7928 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -30,6 +30,7 @@ Distributions for older JTS versions can be obtained at the * Add `Geometry.hasDimension(int dim)` method {#944} * Add `ConcaveHull.alphaShape` function (#952) * Add `OffsetCurve` Joined mode (#956) +* Add `PointLocation.isOnSegment` function (#1048) ### Functionality Improvements * Improve `TopologyPreservingSimplifier` to prevent edge-disjoint line collapse (#925) From eb2943c6656a26c0bf75da917438d3591ed36aa8 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 18 Apr 2024 13:25:01 -0700 Subject: [PATCH 074/123] Add DistanceOp optimization for Point-Point (#1049) --- .../jts/operation/distance/DistanceOp.java | 5 ++ .../distance/PointPointDistancePerfTest.java | 61 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 modules/core/src/test/java/test/jts/perf/operation/distance/PointPointDistancePerfTest.java diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/distance/DistanceOp.java b/modules/core/src/main/java/org/locationtech/jts/operation/distance/DistanceOp.java index cca020c846..5675926d96 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/distance/DistanceOp.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/distance/DistanceOp.java @@ -158,6 +158,11 @@ public double distance() if (geom[0].isEmpty() || geom[1].isEmpty()) return 0.0; + //-- optimization for Point/Point case + if (geom[0] instanceof Point && geom[1] instanceof Point) { + return geom[0].getCoordinate().distance(geom[1].getCoordinate()); + } + computeMinDistance(); return minDistance; } diff --git a/modules/core/src/test/java/test/jts/perf/operation/distance/PointPointDistancePerfTest.java b/modules/core/src/test/java/test/jts/perf/operation/distance/PointPointDistancePerfTest.java new file mode 100644 index 0000000000..7472502dfd --- /dev/null +++ b/modules/core/src/test/java/test/jts/perf/operation/distance/PointPointDistancePerfTest.java @@ -0,0 +1,61 @@ +package test.jts.perf.operation.distance; + +import java.util.ArrayList; +import java.util.List; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; + +import test.jts.perf.PerformanceTestCase; +import test.jts.perf.PerformanceTestRunner; + +public class PointPointDistancePerfTest extends PerformanceTestCase { + + + public static void main(String args[]) { + PerformanceTestRunner.run(PointPointDistancePerfTest.class); + } + + private Point[] grid; + + public PointPointDistancePerfTest(String name) { + super(name); + setRunSize(new int[] {10000}); + setRunIterations(1); + } + + public void startRun(int npts) + { + System.out.println("\n------- Running with # pts = " + npts); + grid = createPointGrid(new Envelope(0, 10., 0, 10), npts); + } + + private Point[] createPointGrid(Envelope envelope, int npts) { + List geoms = new ArrayList(); + GeometryFactory fact = new GeometryFactory(); + int nSide = (int) Math.sqrt(npts); + double xInc = envelope.getWidth() / nSide; + double yInc = envelope.getHeight() / nSide; + for (int i = 0; i < nSide; i++) { + for (int j = 0; j < nSide; j++) { + double x = envelope.getMinX() + i * xInc; + double y = envelope.getMinY() + i * yInc; + Point p = fact.createPoint(new Coordinate(x, y)); + geoms.add(p); + } + } + return GeometryFactory.toPointArray(geoms); + } + + public void runPoints() { + for (Geometry p1 : grid) { + for (Geometry p2 : grid) { + double dist = p1.distance(p2); + } + } + } + +} From 7ef2b9d2e6f36ce5e7a787cff57bd18281e50826 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 18 Apr 2024 13:27:04 -0700 Subject: [PATCH 075/123] Update Version History --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 7f100f7928..82f2c00a0c 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -80,6 +80,7 @@ Distributions for older JTS versions can be obtained at the * Improve Convex Hull performance by avoiding duplicate uniquing (#985) * Improve `HPRtree` performance (#1012) * Improve performance of noding and overlay via `HPRtree` (#1012) +* Improve `DistanceOp` performance for Point-Point (#1049) # Version 1.19 From 773ee336729d443e769e01f0e0821e54e8812a2a Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 16 May 2024 14:48:25 -0700 Subject: [PATCH 076/123] RelateNG algorithm for DE-9IM relationship evaluation (#1052) --- .../function/SelectionNGFunctions.java | 89 +++ .../function/SpatialPredicateFunctions.java | 22 +- .../function/SpatialPredicateNGFunctions.java | 82 +++ .../GeometryFunctionRegistry.java | 4 + .../jts/algorithm/PolygonNodeTopology.java | 1 + .../MCIndexSegmentSetMutualIntersector.java | 15 +- .../relateng/AdjacentEdgeLocator.java | 117 ++++ .../operation/relateng/BasicPredicate.java | 108 ++++ .../operation/relateng/DimensionLocation.java | 82 +++ .../relateng/EdgeSegmentIntersector.java | 89 +++ .../relateng/EdgeSegmentOverlapAction.java | 36 ++ .../relateng/EdgeSetIntersector.java | 82 +++ .../operation/relateng/IMPatternMatcher.java | 107 ++++ .../jts/operation/relateng/IMPredicate.java | 121 ++++ .../relateng/IntersectionMatrixPattern.java | 63 ++ .../operation/relateng/LinearBoundary.java | 87 +++ .../jts/operation/relateng/NodeSection.java | 195 ++++++ .../jts/operation/relateng/NodeSections.java | 122 ++++ .../relateng/PolygonNodeConverter.java | 148 +++++ .../jts/operation/relateng/RelateEdge.java | 366 +++++++++++ .../operation/relateng/RelateGeometry.java | 378 +++++++++++ .../relateng/RelateMatrixPredicate.java | 50 ++ .../jts/operation/relateng/RelateNG.java | 505 +++++++++++++++ .../jts/operation/relateng/RelateNode.java | 232 +++++++ .../relateng/RelatePointLocator.java | 316 ++++++++++ .../operation/relateng/RelatePredicate.java | 596 ++++++++++++++++++ .../relateng/RelateSegmentString.java | 158 +++++ .../operation/relateng/TopologyComputer.java | 506 +++++++++++++++ .../operation/relateng/TopologyPredicate.java | 129 ++++ .../relateng/TopologyPredicateTracer.java | 121 ++++ .../jts/operation/relateng/package-info.java | 99 +++ .../relateng/AdjacentEdgeLocatorTest.java | 85 +++ .../relateng/PolygonNodeConverterTest.java | 154 +++++ .../RelateNGBoundaryNodeRuleTest.java | 131 ++++ .../operation/relateng/RelateNGGCTest.java | 212 +++++++ .../relateng/RelateNGRobustnessTest.java | 195 ++++++ .../jts/operation/relateng/RelateNGTest.java | 589 +++++++++++++++++ .../operation/relateng/RelateNGTestCase.java | 94 +++ .../relateng/RelatePointLocatorTest.java | 87 +++ .../RelateNGLinesOverlappingPerfTest.java | 159 +++++ .../RelateNGPolygonPointsPerfTest.java | 177 ++++++ .../RelateNGPolygonsAdjacentPerfTest.java | 206 ++++++ .../RelateNGPolygonsOverlappingPerfTest.java | 199 ++++++ .../resources/testxml/misc/TestRelateGC.xml | 537 ++++++++++++++++ 44 files changed, 7845 insertions(+), 6 deletions(-) create mode 100644 modules/app/src/main/java/org/locationtech/jtstest/function/SelectionNGFunctions.java create mode 100644 modules/app/src/main/java/org/locationtech/jtstest/function/SpatialPredicateNGFunctions.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/AdjacentEdgeLocator.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/BasicPredicate.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/DimensionLocation.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSegmentIntersector.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSegmentOverlapAction.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSetIntersector.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPatternMatcher.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPredicate.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/IntersectionMatrixPattern.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/LinearBoundary.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/NodeSection.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/NodeSections.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/PolygonNodeConverter.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateEdge.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateGeometry.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateMatrixPredicate.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNode.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePointLocator.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePredicate.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateSegmentString.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyComputer.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicate.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateTracer.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/package-info.java create mode 100644 modules/core/src/test/java/org/locationtech/jts/operation/relateng/AdjacentEdgeLocatorTest.java create mode 100644 modules/core/src/test/java/org/locationtech/jts/operation/relateng/PolygonNodeConverterTest.java create mode 100644 modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGBoundaryNodeRuleTest.java create mode 100644 modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGGCTest.java create mode 100644 modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGRobustnessTest.java create mode 100644 modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTest.java create mode 100644 modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTestCase.java create mode 100644 modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelatePointLocatorTest.java create mode 100644 modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGLinesOverlappingPerfTest.java create mode 100644 modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonPointsPerfTest.java create mode 100644 modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonsAdjacentPerfTest.java create mode 100644 modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonsOverlappingPerfTest.java create mode 100644 modules/tests/src/test/resources/testxml/misc/TestRelateGC.xml diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionNGFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionNGFunctions.java new file mode 100644 index 0000000000..ab2bf55dfc --- /dev/null +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionNGFunctions.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2024 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ + +package org.locationtech.jtstest.function; + +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.operation.relateng.IntersectionMatrixPattern; +import org.locationtech.jts.operation.relateng.RelateNG; +import org.locationtech.jts.operation.relateng.RelatePredicate; + +public class SelectionNGFunctions +{ + public static Geometry intersects(Geometry a, final Geometry mask) + { + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return RelateNG.relate(mask, g, RelatePredicate.intersects()); + } + }); + } + + public static Geometry intersectsPrep(Geometry a, final Geometry mask) + { + RelateNG relateNG = RelateNG.prepare(mask); + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return relateNG.evaluate(g, RelatePredicate.intersects()); + } + }); + } + + public static Geometry contains(Geometry a, final Geometry mask) + { + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return RelateNG.relate(mask, g, RelatePredicate.contains()); + } + }); + } + + public static Geometry covers(Geometry a, final Geometry mask) + { + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return RelateNG.relate(mask, g, RelatePredicate.covers()); + } + }); + } + + public static Geometry coversPrep(Geometry a, final Geometry mask) + { + RelateNG relateNG = RelateNG.prepare(mask); + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return relateNG.evaluate(g, RelatePredicate.covers()); + } + }); + } + + public static Geometry adjacent(Geometry a, final Geometry mask) + { + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return RelateNG.relate(mask, g, RelatePredicate.matches(IntersectionMatrixPattern.ADJACENT)); + } + }); + } + + public static Geometry adjacentPrep(Geometry a, final Geometry mask) + { + RelateNG relateNG = RelateNG.prepare(mask); + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return relateNG.evaluate(g, RelatePredicate.matches(IntersectionMatrixPattern.ADJACENT)); + } + }); + } +} + + diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/SpatialPredicateFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/SpatialPredicateFunctions.java index 7e81aaa71a..f4e8b5f0a7 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/SpatialPredicateFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/SpatialPredicateFunctions.java @@ -14,6 +14,8 @@ import org.locationtech.jts.algorithm.BoundaryNodeRule; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.operation.relate.RelateOp; +import org.locationtech.jts.operation.relateng.IntersectionMatrixPattern; +import org.locationtech.jts.operation.relateng.RelateNG; /** * Implementations for spatial predicate functions. @@ -34,12 +36,24 @@ public class SpatialPredicateFunctions { public static boolean overlaps(Geometry a, Geometry b) { return a.overlaps(b); } public static boolean touches(Geometry a, Geometry b) { return a.touches(b); } - public static boolean interiorIntersects(Geometry a, Geometry b) { return a.relate(b, "T********"); } - public static boolean adjacentTo(Geometry a, Geometry b) { return a.relate(b, "F***T****"); } - - public static String relate(Geometry a, Geometry b) { + public static boolean interiorIntersects(Geometry a, Geometry b) { + return a.relate(b, IntersectionMatrixPattern.INTERIOR_INTERSECTS); + } + + public static boolean adjacent(Geometry a, Geometry b) { + return a.relate(b, IntersectionMatrixPattern.ADJACENT); + } + + public static boolean containsProperly(Geometry a, Geometry b) { + return a.relate(b, IntersectionMatrixPattern.CONTAINS_PROPERLY); + } + + public static String relateMatrix(Geometry a, Geometry b) { return a.relate(b).toString(); } + public static boolean relate(Geometry a, Geometry b, String mask) { + return a.relate(b, mask); + } public static String relateEndpoint(Geometry a, Geometry b) { return RelateOp.relate(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE).toString(); } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/SpatialPredicateNGFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/SpatialPredicateNGFunctions.java new file mode 100644 index 0000000000..17f01c2404 --- /dev/null +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/SpatialPredicateNGFunctions.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jtstest.function; + +import org.locationtech.jts.algorithm.BoundaryNodeRule; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.operation.relateng.IntersectionMatrixPattern; +import org.locationtech.jts.operation.relateng.RelateNG; +import org.locationtech.jts.operation.relateng.RelatePredicate; + +public class SpatialPredicateNGFunctions { + public static boolean contains(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.contains()); + } + public static boolean covers(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.covers()); + } + public static boolean coveredBy(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.coveredBy()); + } + public static boolean disjoint(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.disjoint()); + } + public static boolean equals(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.equalsTopo()); + } + public static boolean equalsTopo(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.equalsTopo()); + } + public static boolean intersects(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.intersects()); + } + public static boolean crosses(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.crosses()); + } + public static boolean overlaps(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.overlaps()); + } + public static boolean touches(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.touches()); + } + public static boolean within(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.within()); + } + + public static boolean adjacent(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.matches(IntersectionMatrixPattern.ADJACENT)); + } + + public static boolean containsProperly(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.matches(IntersectionMatrixPattern.CONTAINS_PROPERLY)); + } + + public static boolean interiorIntersects(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.matches(IntersectionMatrixPattern.INTERIOR_INTERSECTS)); + } + + public static boolean relate(Geometry a, Geometry b, String mask) { + return RelateNG.relate(a, b, mask); + } + public static String relateMatrix(Geometry a, Geometry b) { + return RelateNG.relate(a, b).toString(); + } + public static String relateEndpoint(Geometry a, Geometry b) { + return RelateNG.relate(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE).toString(); + } + public static String relateMultiValent(Geometry a, Geometry b) { + return RelateNG.relate(a, b, BoundaryNodeRule.MULTIVALENT_ENDPOINT_BOUNDARY_RULE).toString(); + } + public static String relateMonoValent(Geometry a, Geometry b) { + return RelateNG.relate(a, b, BoundaryNodeRule.MONOVALENT_ENDPOINT_BOUNDARY_RULE).toString(); + } +} diff --git a/modules/app/src/main/java/org/locationtech/jtstest/geomfunction/GeometryFunctionRegistry.java b/modules/app/src/main/java/org/locationtech/jtstest/geomfunction/GeometryFunctionRegistry.java index 568e0f419d..319bfb67a3 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/geomfunction/GeometryFunctionRegistry.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/geomfunction/GeometryFunctionRegistry.java @@ -61,11 +61,13 @@ import org.locationtech.jtstest.function.PrecisionFunctions; import org.locationtech.jtstest.function.PreparedGeometryFunctions; import org.locationtech.jtstest.function.SelectionFunctions; +import org.locationtech.jtstest.function.SelectionNGFunctions; import org.locationtech.jtstest.function.SimplificationFunctions; import org.locationtech.jtstest.function.SnappingFunctions; import org.locationtech.jtstest.function.SortingFunctions; import org.locationtech.jtstest.function.SpatialIndexFunctions; import org.locationtech.jtstest.function.SpatialPredicateFunctions; +import org.locationtech.jtstest.function.SpatialPredicateNGFunctions; import org.locationtech.jtstest.function.TriangleFunctions; import org.locationtech.jtstest.function.TriangulatePolyFunctions; import org.locationtech.jtstest.function.TriangulationFunctions; @@ -102,6 +104,7 @@ public static GeometryFunctionRegistry createTestBuilderRegistry() funcRegistry.add(PrecisionFunctions.class); funcRegistry.add(PreparedGeometryFunctions.class); funcRegistry.add(SelectionFunctions.class); + funcRegistry.add(SelectionNGFunctions.class); funcRegistry.add(SimplificationFunctions.class); funcRegistry.add(AffineTransformationFunctions.class); funcRegistry.add(DiffFunctions.class); @@ -112,6 +115,7 @@ public static GeometryFunctionRegistry createTestBuilderRegistry() funcRegistry.add(CreateRandomShapeFunctions.class); funcRegistry.add(SpatialIndexFunctions.class); funcRegistry.add(SpatialPredicateFunctions.class); + funcRegistry.add(SpatialPredicateNGFunctions.class); funcRegistry.add(JTSFunctions.class); //funcRegistry.add(MemoryFunctions.class); funcRegistry.add(OffsetCurveFunctions.class); diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/PolygonNodeTopology.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/PolygonNodeTopology.java index c96777878b..de47f251e5 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/PolygonNodeTopology.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/PolygonNodeTopology.java @@ -162,6 +162,7 @@ private static boolean isAngleGreater(Coordinate origin, Coordinate p, Coordinat /** * Compares the angles of two vectors * relative to the positive X-axis at their origin. + * Angles increase CCW from the X-axis. * * @param origin the origin of the vectors * @param p the endpoint of the vector P diff --git a/modules/core/src/main/java/org/locationtech/jts/noding/MCIndexSegmentSetMutualIntersector.java b/modules/core/src/main/java/org/locationtech/jts/noding/MCIndexSegmentSetMutualIntersector.java index c5038b2dfc..f1ee7af509 100644 --- a/modules/core/src/main/java/org/locationtech/jts/noding/MCIndexSegmentSetMutualIntersector.java +++ b/modules/core/src/main/java/org/locationtech/jts/noding/MCIndexSegmentSetMutualIntersector.java @@ -42,6 +42,7 @@ public class MCIndexSegmentSetMutualIntersector implements SegmentSetMutualInter */ private STRtree index = new STRtree(); private double overlapTolerance = 0.0; + private Envelope envelope = null; /** * Constructs a new intersector for a given set of {@link SegmentString}s. @@ -53,6 +54,12 @@ public MCIndexSegmentSetMutualIntersector(Collection baseSegStrings) initBaseSegments(baseSegStrings); } + public MCIndexSegmentSetMutualIntersector(Collection baseSegStrings, Envelope env) + { + this.envelope = env; + initBaseSegments(baseSegStrings); + } + public MCIndexSegmentSetMutualIntersector(Collection baseSegStrings, double overlapTolerance) { initBaseSegments(baseSegStrings); @@ -84,7 +91,9 @@ private void addToIndex(SegmentString segStr) List segChains = MonotoneChainBuilder.getChains(segStr.getCoordinates(), segStr); for (Iterator i = segChains.iterator(); i.hasNext(); ) { MonotoneChain mc = (MonotoneChain) i.next(); - index.insert(mc.getEnvelope(overlapTolerance), mc); + if (envelope == null || envelope.intersects(mc.getEnvelope())) { + index.insert(mc.getEnvelope(overlapTolerance), mc); + } } } @@ -114,7 +123,9 @@ private void addToMonoChains(SegmentString segStr, List monoChains) List segChains = MonotoneChainBuilder.getChains(segStr.getCoordinates(), segStr); for (Iterator i = segChains.iterator(); i.hasNext(); ) { MonotoneChain mc = (MonotoneChain) i.next(); - monoChains.add(mc); + if (envelope == null || envelope.intersects(mc.getEnvelope())) { + monoChains.add(mc); + } } } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/AdjacentEdgeLocator.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/AdjacentEdgeLocator.java new file mode 100644 index 0000000000..e66a68cf5e --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/AdjacentEdgeLocator.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2024 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import java.util.ArrayList; +import java.util.List; + +import org.locationtech.jts.algorithm.PointLocation; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.Location; +import org.locationtech.jts.geom.Polygon; + +/** + * Determines the location for a point which is known to lie + * on at least one edge of a set of polygons. + * This provides the union-semantics for determining + * point location in a GeometryCollection, which may + * have polygons with adjacent edges which are effectively + * in the interior of the geometry. + * Note that it is also possible to have adjacent edges which + * lie on the boundary of the geometry + * (e.g. a polygon contained within another polygon with adjacent edges). + * + * @author mdavis + * + */ +class AdjacentEdgeLocator { + + private List ringList;; + + public AdjacentEdgeLocator(Geometry geom) { + init(geom); + } + + public int locate(Coordinate p) { + NodeSections sections = new NodeSections(p); + for (Coordinate[] ring : ringList) { + addSections(p, ring, sections); + } + RelateNode node = sections.createNode(); + //node.finish(false, false); + return node.hasExteriorEdge(true) ? Location.BOUNDARY : Location.INTERIOR; + } + + private void addSections(Coordinate p, Coordinate[] ring, NodeSections sections) { + for (int i = 0; i < ring.length - 1; i++) { + Coordinate p0 = ring[i]; + Coordinate pnext = ring[i + 1]; + + if (p.equals2D(pnext)) { + //-- segment final point is assigned to next segment + continue; + } + else if (p.equals2D(p0)) { + int iprev = i > 0 ? i - 1 : ring.length - 2; + Coordinate pprev = ring[iprev]; + sections.addNodeSection(createSection(p, pprev, pnext)); + } + else if (PointLocation.isOnSegment(p, p0, pnext)) { + sections.addNodeSection(createSection(p, p0, pnext)); + } + } + } + + private NodeSection createSection(Coordinate p, Coordinate prev, Coordinate next) { + if (prev.distance(p) == 0 || next.distance(p) == 0) { + System.out.println("Found zero-length section segment"); + }; + NodeSection ns = new NodeSection(true, Dimension.A, 1, 0, null, false, prev, p, next); + return ns; + } + + private void init(Geometry geom) { + if (geom.isEmpty()) + return; + ringList = new ArrayList(); + addRings(geom, ringList); + } + + private void addRings(Geometry geom, List ringList2) { + if (geom instanceof Polygon) { + Polygon poly = (Polygon) geom; + LinearRing shell = poly.getExteriorRing(); + addRing(shell, true); + for (int i = 0; i < poly.getNumInteriorRing(); i++) { + LinearRing hole = poly.getInteriorRingN(i); + addRing(hole, false); + } + } + else if (geom instanceof GeometryCollection) { + //-- recurse through collections + for (int i = 0; i < geom.getNumGeometries(); i++) { + addRings(geom.getGeometryN(i), ringList); + } + } + } + + private void addRing(LinearRing ring, boolean requireCW) { + //TODO: remove repeated points? + Coordinate[] pts = RelateGeometry.orient(ring.getCoordinates(), requireCW); + ringList.add(pts); + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/BasicPredicate.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/BasicPredicate.java new file mode 100644 index 0000000000..cc0260c1ed --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/BasicPredicate.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Location; + +/** + * The base class for relate topological predicates + * with a boolean value. + * Implements tri-state logic for the predicate value, + * to detect when the final value has been determined. + * + * @author Martin Davis + * + */ +abstract class BasicPredicate implements TopologyPredicate { + + private static final int UNKNOWN = -1; + private static final int FALSE = 0; + private static final int TRUE = 1; + + private static boolean isKnown(int value) { + return value > UNKNOWN; + } + + private static boolean toBoolean(int value) { + return value == TRUE; + } + + private static int toValue(boolean val) { + return val ? TRUE : FALSE; + } + + /** + * Tests if two geometries intersect + * based on an interaction at given locations. + * + * @param locA the location on geometry A + * @param locB the location on geometry B + * @return true if the geometries intersect + */ + public static boolean isIntersection(int locA, int locB) { + //-- i.e. some location on both geometries intersects + return locA != Location.EXTERIOR && locB != Location.EXTERIOR; + } + + private int value = UNKNOWN; + + /* + public boolean isSelfNodingRequired() { + return false; + } + */ + + @Override + public boolean isKnown() { + return isKnown(value); + } + + @Override + public boolean value() { + return toBoolean(value); + } + + /** + * Updates the predicate value to the given state + * if it is currently unknown. + * + * @param val the predicate value to update + */ + protected void setValue(boolean val) { + //-- don't change already-known value + if (isKnown()) + return; + value = toValue(val); + } + + protected void setValue(int val) { + //-- don't change already-known value + if (isKnown()) + return; + value = val; + } + + protected void setValueIf(boolean value, boolean cond) { + if (cond) + setValue(value); + } + + protected void require(boolean cond) { + if (! cond) + setValue(false); + } + + protected void requireCovers(Envelope a, Envelope b) { + require(a.covers(b)); + } +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/DimensionLocation.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/DimensionLocation.java new file mode 100644 index 0000000000..6aa32b286d --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/DimensionLocation.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Location; + +class DimensionLocation { + + public static final int EXTERIOR = Location.EXTERIOR; + public static final int POINT_INTERIOR = 103; + public static final int LINE_INTERIOR = 110; + public static final int LINE_BOUNDARY = 111; + public static final int AREA_INTERIOR = 120; + public static final int AREA_BOUNDARY = 121; + + public static int locationArea(int loc) { + switch (loc) { + case Location.INTERIOR: return AREA_INTERIOR; + case Location.BOUNDARY: return AREA_BOUNDARY; + } + return EXTERIOR; + } + + public static int locationLine(int loc) { + switch (loc) { + case Location.INTERIOR: return LINE_INTERIOR; + case Location.BOUNDARY: return LINE_BOUNDARY; + } + return EXTERIOR; + } + + public static int locationPoint(int loc) { + switch (loc) { + case Location.INTERIOR: return POINT_INTERIOR; + } + return EXTERIOR; + } + + public static int location(int dimLoc) { + switch (dimLoc) { + case POINT_INTERIOR: + case LINE_INTERIOR: + case AREA_INTERIOR: + return Location.INTERIOR; + case LINE_BOUNDARY: + case AREA_BOUNDARY: + return Location.BOUNDARY; + } + return Location.EXTERIOR; + } + + public static int dimension(int dimLoc) { + switch (dimLoc) { + case POINT_INTERIOR: + return Dimension.P; + case LINE_INTERIOR: + case LINE_BOUNDARY: + return Dimension.L; + case AREA_INTERIOR: + case AREA_BOUNDARY: + return Dimension.A; + } + return Dimension.FALSE; + } + + public static int dimension(int dimLoc, int exteriorDim) { + if (dimLoc == EXTERIOR) + return exteriorDim; + return dimension(dimLoc); + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSegmentIntersector.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSegmentIntersector.java new file mode 100644 index 0000000000..f60e0f6452 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSegmentIntersector.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2022 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import org.locationtech.jts.algorithm.RobustLineIntersector; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.noding.SegmentIntersector; +import org.locationtech.jts.noding.SegmentString; + +/** + * Tests segments of {@link RelateSegmentString}s + * and if they intersect adds the intersection(s) + * to the {@link TopologyComputer}. + * + * @author Martin Davis + * + */ +class EdgeSegmentIntersector implements SegmentIntersector +{ + private RobustLineIntersector li = new RobustLineIntersector(); + private TopologyComputer topoComputer; + + public EdgeSegmentIntersector(TopologyComputer topoBuilder) { + this.topoComputer = topoBuilder; + } + + @Override + public boolean isDone() { + return topoComputer.isResultKnown(); + } + + public void processIntersections(SegmentString ss0, int segIndex0, + SegmentString ss1, int segIndex1) { + // don't intersect a segment with itself + if (ss0 == ss1 && segIndex0 == segIndex1) return; + + RelateSegmentString rss0 = (RelateSegmentString) ss0; + RelateSegmentString rss1 = (RelateSegmentString) ss1; + //TODO: move this ordering logic to TopologyBuilder + if (rss0.isA()) { + addIntersections(rss0, segIndex0, rss1, segIndex1); + } + else { + addIntersections(rss1, segIndex1, rss0, segIndex0); + } + } + + private void addIntersections(RelateSegmentString ssA, int segIndexA, + RelateSegmentString ssB, int segIndexB) { + + Coordinate a0 = ssA.getCoordinate(segIndexA); + Coordinate a1 = ssA.getCoordinate(segIndexA + 1); + Coordinate b0 = ssB.getCoordinate(segIndexB); + Coordinate b1 = ssB.getCoordinate(segIndexB + 1); + + li.computeIntersection(a0, a1, b0, b1); + + if (! li.hasIntersection()) + return; + + for (int i = 0; i < li.getIntersectionNum(); i++) { + Coordinate intPt = li.getIntersection(i); + /** + * Ensure endpoint intersections are added once only, for their canonical segments. + * Proper intersections lie on a unique segment so do not need to be checked. + * And it is important that the Containing Segment check not be used, + * since due to intersection computation roundoff, + * it is not reliable in that situation. + */ + if (li.isProper() + || (ssA.isContainingSegment(segIndexA, intPt) + && ssB.isContainingSegment(segIndexB, intPt))) { + NodeSection nsa = ssA.createNodeSection(segIndexA, intPt); + NodeSection nsb = ssB.createNodeSection(segIndexB, intPt); + topoComputer.addIntersection(nsa, nsb); + } + } + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSegmentOverlapAction.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSegmentOverlapAction.java new file mode 100644 index 0000000000..7a44fd979a --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSegmentOverlapAction.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import org.locationtech.jts.index.chain.MonotoneChain; +import org.locationtech.jts.index.chain.MonotoneChainOverlapAction; +import org.locationtech.jts.noding.SegmentIntersector; +import org.locationtech.jts.noding.SegmentString; + +class EdgeSegmentOverlapAction + extends MonotoneChainOverlapAction +{ + private SegmentIntersector si = null; + + public EdgeSegmentOverlapAction(SegmentIntersector si) + { + this.si = si; + } + + public void overlap(MonotoneChain mc1, int start1, MonotoneChain mc2, int start2) + { + SegmentString ss1 = (SegmentString) mc1.getContext(); + SegmentString ss2 = (SegmentString) mc2.getContext(); + si.processIntersections(ss1, start1, ss2, start2); + } + +} \ No newline at end of file diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSetIntersector.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSetIntersector.java new file mode 100644 index 0000000000..ab6a7c00bf --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSetIntersector.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.index.chain.MonotoneChain; +import org.locationtech.jts.index.chain.MonotoneChainBuilder; +import org.locationtech.jts.index.chain.MonotoneChainOverlapAction; +import org.locationtech.jts.index.hprtree.HPRtree; +import org.locationtech.jts.noding.SegmentString; + +class EdgeSetIntersector { + + private HPRtree index = new HPRtree(); + private Envelope envelope; + private List monoChains = new ArrayList(); + private int idCounter = 0; + + public EdgeSetIntersector(List edgesA, List edgesB, Envelope env) { + this.envelope = env; + addEdges(edgesA); + addEdges(edgesB); + // build index to ensure thread-safety + index.build(); + } + + private void addEdges(Collection segStrings) + { + for (SegmentString ss : segStrings) { + addToIndex(ss); + } + } + + private void addToIndex(SegmentString segStr) + { + List segChains = MonotoneChainBuilder.getChains(segStr.getCoordinates(), segStr); + for (Iterator i = segChains.iterator(); i.hasNext(); ) { + MonotoneChain mc = (MonotoneChain) i.next(); + if (envelope == null || envelope.intersects(mc.getEnvelope())) { + mc.setId(idCounter ++); + index.insert(mc.getEnvelope(), mc); + monoChains.add(mc); + } + } + } + + public void process(EdgeSegmentIntersector intersector) { + MonotoneChainOverlapAction overlapAction = new EdgeSegmentOverlapAction(intersector); + + for (MonotoneChain queryChain : monoChains) { + List overlapChains = index.query(queryChain.getEnvelope()); + for (Iterator j = overlapChains.iterator(); j.hasNext(); ) { + MonotoneChain testChain = (MonotoneChain) j.next(); + /** + * following test makes sure we only compare each pair of chains once + * and that we don't compare a chain to itself + */ + if (testChain.getId() <= queryChain.getId()) + continue; + + testChain.computeOverlaps(queryChain, overlapAction); + if (intersector.isDone()) + return; + } + } + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPatternMatcher.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPatternMatcher.java new file mode 100644 index 0000000000..920dc4d124 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPatternMatcher.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.IntersectionMatrix; +import org.locationtech.jts.geom.Location; + +/** + * A predicate that matches a DE-9IM pattern. + * + *

FUTURE WORK

+ * Extend the expressiveness of the DE-9IM pattern language to allow: + *
    + *
  • Combining patterns via disjunction using "|". + *
  • Limiting patterns via geometry dimension. + * A dimension limit specifies the allowable dimensions + * for both or individual geometries as [d] or [ab] or [ab;cd] + *
+ * + * @author Martin Davis + * + */ +class IMPatternMatcher extends IMPredicate +{ + private String imPattern = null; + private IntersectionMatrix patternMatrix; + + public IMPatternMatcher(String imPattern) { + this.imPattern = imPattern; + this.patternMatrix = new IntersectionMatrix(imPattern); + } + + public String name() { return "IMPattern"; } + + //TODO: implement requiresExteriorCheck by inspecting matrix entries for E + + public void init(Envelope envA, Envelope envB) { + super.init(dimA, dimB); + //-- if pattern specifies any non-E/non-E interaction, envelopes must not be disjoint + boolean requiresInteraction = requiresInteraction(patternMatrix); + boolean isDisjoint = envA.disjoint(envB); + setValueIf(false, requiresInteraction && isDisjoint); + } + + private static boolean requiresInteraction(IntersectionMatrix im) { + boolean requiresInteraction = + requiresInteraction(im.get(Location.INTERIOR, Location.INTERIOR)) + || requiresInteraction(im.get(Location.INTERIOR, Location.BOUNDARY)) + || requiresInteraction(im.get(Location.BOUNDARY, Location.INTERIOR)) + || requiresInteraction(im.get(Location.BOUNDARY, Location.BOUNDARY)); + return requiresInteraction; + } + + private static boolean requiresInteraction(int imDim) { + return imDim == Dimension.TRUE || imDim >= Dimension.P; + } + + @Override + public boolean isDetermined() { + /** + * Matrix entries only increase in dimension as topology is computed. + * The predicate can be short-circuited (as false) if + * any computed entry is greater than the mask value. + */ + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + int patternEntry = patternMatrix.get(i, j); + + if (patternEntry == Dimension.DONTCARE) + continue; + + int matrixVal = getDimension(i, j); + + //-- mask entry TRUE requires a known matrix entry + if (patternEntry == Dimension.TRUE) { + if (matrixVal < 0) + return false; + } + //-- result is known (false) if matrix entry has exceeded mask + else if (matrixVal > patternEntry) + return true; + } + } + return false; + } + + @Override + public boolean valueIM() { + boolean val = intMatrix.matches(imPattern); + return val; + } + + public String toString() { + return name() + "(" + imPattern + ")"; + } +} \ No newline at end of file diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPredicate.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPredicate.java new file mode 100644 index 0000000000..9ac1f27f3e --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPredicate.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.IntersectionMatrix; +import org.locationtech.jts.geom.Location; + +/** + * A base class for predicates which are + * determined using entries in a {@link IntersectionMatrix}. + * + * @author Martin Davis + * + */ +abstract class IMPredicate extends BasicPredicate { + + public static boolean isDimsCompatibleWithCovers(int dim0, int dim1) { + //- allow Points coveredBy zero-length Lines + if (dim0 == Dimension.P && dim1 == Dimension.L) + return true; + return dim0 >= dim1; + } + + static final int DIM_UNKNOWN = Dimension.DONTCARE; + + protected int dimA; + protected int dimB; + protected IntersectionMatrix intMatrix; + + public IMPredicate() { + intMatrix = new IntersectionMatrix(); + //-- E/E is always dim = 2 + intMatrix.set(Location.EXTERIOR, Location.EXTERIOR, Dimension.A); + } + + @Override + public void init(int dimA, int dimB) { + this.dimA = dimA; + this.dimB = dimB; + } + + @Override + public void updateDimension(int locA, int locB, int dimension) { + //-- only record an increased dimension value + if (isDimChanged(locA, locB, dimension)) { + intMatrix.set(locA, locB, dimension); + //-- set value if predicate value can be known + if (isDetermined()) { + setValue( valueIM()); + } + } + } + + public boolean isDimChanged(int locA, int locB, int dimension) { + return dimension > intMatrix.get(locA, locB); + } + + /** + * Tests whether predicate evaluation can be short-circuited + * due to the current state of the matrix providing + * enough information to determine the predicate value. + *

+ * If this value is true then {@link valueIM()} + * must provide the correct result of the predicate. + * + * @return true if the predicate value is determined + */ + protected abstract boolean isDetermined(); + + protected boolean intersectsExteriorOf(boolean isA) { + if (isA) { + return isIntersects(Location.EXTERIOR, Location.INTERIOR) + || isIntersects(Location.EXTERIOR, Location.BOUNDARY); + } + else { + return isIntersects(Location.INTERIOR, Location.EXTERIOR) + || isIntersects(Location.BOUNDARY, Location.EXTERIOR); + } + } + + protected boolean isIntersects(int locA, int locB) { + return intMatrix.get(locA, locB) >= Dimension.P; + } + + public boolean isKnown(int locA, int locB) { + return intMatrix.get(locA, locB) != DIM_UNKNOWN; + } + + public boolean isDimension(int locA, int locB, int dimension) { + return intMatrix.get(locA, locB) == dimension; + } + + public int getDimension(int locA, int locB) { + return intMatrix.get(locA, locB); + } + + /** + * Sets the final value based on the state of the IM. + */ + @Override + public void finish() { + setValue(valueIM()); + } + + protected abstract boolean valueIM(); + + public String toString() { + return name() + ": " + intMatrix; + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IntersectionMatrixPattern.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IntersectionMatrixPattern.java new file mode 100644 index 0000000000..3e7e6dffb3 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IntersectionMatrixPattern.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +/** + * String constants for DE-9IM matrix patterns for topological relationships. + * These can be used with {@link RelateNG#evaluate(org.locationtech.jts.geom.Geometry, String)} + * and {@link RelateNG#relate(org.locationtech.jts.geom.Geometry, org.locationtech.jts.geom.Geometry, String)}. + * + *

DE-9IM Pattern Matching

+ * Matrix patterns are specified as a 9-character string + * containing the pattern symbols for the DE-9IM 3x3 matrix entries, + * listed row-wise. + * The pattern symbols are: + *
    + *
  • 0 - topological interaction has dimension 0 + *
  • 1 - topological interaction has dimension 1 + *
  • 2 - topological interaction has dimension 2 + *
  • F - no topological interaction + *
  • T - topological interaction of any dimension + *
  • * - any topological interaction is allowed, including none + *
+ * + * @author Martin Davis + * + */ +public class IntersectionMatrixPattern { + + /** + * A DE-9IM pattern to detect whether two polygonal geometries are adjacent along + * an edge, but do not overlap. + */ + public static final String ADJACENT = "F***1****"; + + /** + * A DE-9IM pattern to detect a geometry which properly contains another + * geometry (i.e. which lies entirely in the interior of the first geometry). + */ + public static final String CONTAINS_PROPERLY = "T**FF*FF*"; + + /** + * A DE-9IM pattern to detect if two geometries intersect in their interiors. + * This can be used to determine if a polygonal coverage contains any overlaps + * (although not whether they are correctly noded). + */ + public static final String INTERIOR_INTERSECTS = "T********"; + + /** + * Cannot be instantiated. + */ + private IntersectionMatrixPattern() { + + } +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/LinearBoundary.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/LinearBoundary.java new file mode 100644 index 0000000000..8267ae1394 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/LinearBoundary.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2024 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.locationtech.jts.algorithm.BoundaryNodeRule; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; + +/** + * Determines the boundary points of a linear geometry, + * using a {@link BoundaryNodeRule}. + * + * @author mdavis + * + */ +class LinearBoundary { + + private Map vertexDegree = new HashMap(); + private boolean hasBoundary; + private BoundaryNodeRule boundaryNodeRule; + + public LinearBoundary(List lines, BoundaryNodeRule bnRule) { + //assert: dim(geom) == 1 + this.boundaryNodeRule = bnRule; + vertexDegree = computeBoundaryPoints(lines); + hasBoundary = checkBoundary(vertexDegree); + } + + private boolean checkBoundary(Map vertexDegree) { + for (int degree : vertexDegree.values()) { + if (boundaryNodeRule.isInBoundary(degree)) { + return true; + } + } + return false; + } + + public boolean isBoundary(Coordinate pt) { + if (! vertexDegree.containsKey(pt)) + return false; + int degree = vertexDegree.get(pt); + return boundaryNodeRule.isInBoundary(degree); + } + + private static Map computeBoundaryPoints(List lines) { + Map vertexDegree = new HashMap(); + for (LineString line : lines) { + if (line.isEmpty()) + continue; + addEndpoint(line.getCoordinateN(0), vertexDegree); + addEndpoint(line.getCoordinateN(line.getNumPoints() - 1), vertexDegree); + } + return vertexDegree; + } + + private static void addEndpoint(Coordinate p, Map degree) { + int dim = 0; + if (degree.containsKey(p)) { + dim = degree.get(p); + } + dim++; + degree.put(p, dim); + } + + public Set getEndPoints() { + return vertexDegree.keySet(); + } + + public boolean hasBoundary() { + return hasBoundary; + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/NodeSection.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/NodeSection.java new file mode 100644 index 0000000000..dd59d7bf50 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/NodeSection.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2024 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import java.util.Comparator; + +import org.locationtech.jts.algorithm.PolygonNodeTopology; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.io.WKTWriter; + +/** + * Represents a computed node along with the incident edges on either side of + * it (if they exist). + * This captures the information about a node in a geometry component + * required to determine the component's contribution to the node topology. + * A node in an area geometry always has edges on both sides of the node. + * A node in a linear geometry may have one or other incident edge missing, if + * the node occurs at an endpoint of the line. + * The edges of an area node are assumed to be provided + * with CW-shell orientation (as per JTS norm). + * This must be enforced by the caller. + * + * @author Martin Davis + * + */ +class NodeSection implements Comparable +{ + /** + * Compares sections by the angle the entering edge makes with the positive X axis. + */ + public static class EdgeAngleComparator implements Comparator { + + @Override + public int compare(NodeSection ns1, NodeSection ns2) { + return PolygonNodeTopology.compareAngle(ns1.nodePt, ns1.getVertex(0), ns2.getVertex(0)); + } + } + + public static boolean isAreaArea(NodeSection a, NodeSection b) { + return a.dimension() == Dimension.A && b.dimension() == Dimension.A; + } + + private boolean isA; + private int dim; + private int id; + private int ringId; + private boolean isNodeAtVertex; + private Coordinate nodePt; + private Coordinate v0; + private Coordinate v1; + private Geometry poly; + + public NodeSection(boolean isA, + int dimension, int id, int ringId, + Geometry poly, boolean isNodeAtVertex, Coordinate v0, Coordinate nodePt, Coordinate v1) { + this.isA = isA; + this.dim = dimension; + this.id = id; + this.ringId = ringId; + this.poly = poly; + this.isNodeAtVertex = isNodeAtVertex; + this.nodePt = nodePt; + this.v0 = v0; + this.v1 = v1; + } + + public Coordinate getVertex(int i) { + return i == 0 ? v0 : v1; + } + + public Coordinate nodePt() { + return nodePt; + } + + public int dimension() { + return dim; + } + + public int id() { + return id; + } + + public int ringId() { + return ringId; + } + + public Geometry getPolygonal() { + return poly; + } + + public boolean isShell() { + return ringId == 0; + } + + public boolean isArea() { + return dim == Dimension.A; + } + + public boolean isA() { + return isA; + } + + public boolean isSameGeometry(NodeSection ns) { + return isA() == ns.isA(); + } + + public boolean isSamePolygon(NodeSection ns) { + return isA() == ns.isA() && id() == ns.id(); + } + + public boolean isNodeAtVertex() { + return isNodeAtVertex; + } + + public boolean isProper() { + return ! isNodeAtVertex; + } + + public static boolean isProper(NodeSection a, NodeSection b) { + return a.isProper() && b.isProper(); + } + + public String toString() { + String geomName = RelateGeometry.name(isA); + String atVertexInd = isNodeAtVertex ? "-V-" : "---"; + String polyId = id >= 0 ? "[" + id + ":" + ringId + "]" : ""; + return String.format("%s%d%s: %s %s %s", + geomName, dim, polyId, edgeRep(v0, nodePt), atVertexInd, edgeRep(nodePt, v1)); + } + + private String edgeRep(Coordinate p0, Coordinate p1) { + if (p0 == null || p1 == null) + return "null"; + return WKTWriter.toLineString(p0, p1); + } + + /** + * Compare node sections by parent geometry, dimension, element id and ring id, + * and edge vertices. + * Sections are assumed to be at the same node point. + */ + @Override + public int compareTo(NodeSection o) { + // Assert: nodePt.equals2D(o.nodePt()) + + // sort A before B + if (isA != o.isA) { + if (isA) return -1; + return 1; + } + //-- sort on dimensions + int compDim = Integer.compare(dim, o.dim); + if (compDim != 0) return compDim; + + //-- sort on id and ring id + int compId = Integer.compare(id, o.id); + if (compId != 0) return compId; + + int compRingId = Integer.compare(ringId, o.ringId); + if (compRingId != 0) return compRingId; + + //-- sort on edge coordinates + int compV0 = compareWithNull(v0, o.v0); + if (compV0 != 0) return compV0; + + return compareWithNull(v1, o.v1); + } + + private static int compareWithNull(Coordinate v0, Coordinate v1) { + if (v0 == null) { + if (v1 == null) + return 0; + //-- null is lower than non-null + return -1; + } + // v0 is non-null + if (v1 == null) + return 1; + return v0.compareTo(v1); + } + + + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/NodeSections.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/NodeSections.java new file mode 100644 index 0000000000..c1a7ea96f0 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/NodeSections.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2024 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import java.util.ArrayList; +import java.util.List; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; + +class NodeSections { + + private Coordinate nodePt; + + private List sections = new ArrayList();; + + public NodeSections(Coordinate pt) { + this.nodePt = pt; + } + + public Coordinate getCoordinate() { + return nodePt; + } + + public void addNodeSection(NodeSection e) { +//System.out.println(e); + sections.add(e); + } + + public boolean hasInteractionAB() { + boolean isA = false; + boolean isB = false; + for (NodeSection ns : sections) { + if (ns.isA()) + isA = true; + else + isB = true; + if (isA && isB) + return true; + } + return false; + } + + + public Geometry getPolygonal(boolean isA) { + for (NodeSection ns : sections) { + if (ns.isA() == isA) { + Geometry poly = ns.getPolygonal(); + if (poly != null) + return poly; + } + } + return null; + } + + public RelateNode createNode() { + prepareSections(); + + RelateNode node = new RelateNode(nodePt); + int i = 0; + while (i < sections.size()) { + NodeSection ns = sections.get(i); + //-- if there multiple polygon sections incident at node convert them to maximal-ring structure + if (ns.isArea() && hasMultiplePolygonSections(sections, i)) { + List polySections = collectPolygonSections(sections, i); + List nsConvert = PolygonNodeConverter.convert(polySections); + node.addEdges(nsConvert); + i += polySections.size(); + } + else { + //-- the most common case is a line or a single polygon ring section + node.addEdges(ns); + i += 1; + } + } + return node; + } + + /** + * Sorts the sections so that: + *
    + *
  • lines are before areas + *
  • edges from the same polygon are contiguous + *
+ */ + private void prepareSections() { + sections.sort(null); + //TODO: remove duplicate sections + } + + private static boolean hasMultiplePolygonSections(List sections, int i) { + //-- if last section can only be one + if (i >= sections.size() - 1) + return false; + //-- check if there are at least two sections for same polygon + NodeSection ns = sections.get(i); + NodeSection nsNext = sections.get(i + 1); + return ns.isSamePolygon(nsNext); + } + + private static List collectPolygonSections(List sections, int i) { + List polySections = new ArrayList(); + //-- note ids are only unique to a geometry + NodeSection polySection = sections.get(i); + while (i < sections.size() && + polySection.isSamePolygon(sections.get(i))) { + polySections.add(sections.get(i)); + i++; + } + return polySections; + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/PolygonNodeConverter.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/PolygonNodeConverter.java new file mode 100644 index 0000000000..079e30023e --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/PolygonNodeConverter.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import java.util.ArrayList; +import java.util.List; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Dimension; + +/** + * Converts the node sections at a polygon node where + * a shell and one or more holes touch, or two or more holes touch. + * This converts the node topological structure from + * the OGC "touching-rings" (AKA "minimal-ring") model to the equivalent "self-touch" + * (AKA "inverted/exverted ring" or "maximal ring") model. + * In the "self-touch" model the converted NodeSection corners enclose areas + * which all lies inside the polygon + * (i.e. they does not enclose hole edges). + * This allows {@link RelateNode} to use simple area-additive semantics + * for adding edges and propagating edge locations. + *

+ * The input node sections are assumed to have canonical orientation + * (CW shells and CCW holes). + * The arrangement of shells and holes must be topologically valid. + * Specifically, the node sections must not cross or be collinear. + *

+ * This supports multiple shell-shell touches + * (including ones containing holes), and hole-hole touches, + * This generalizes the relate algorithm to support + * both the OGC model and the self-touch model. + * + * @author Martin Davis + * @see RelateNode + */ +class PolygonNodeConverter { + + /** + * Converts a list of sections of valid polygon rings + * to have "self-touching" structure. + * There are the same number of output sections as input ones. + * + * @param polySections the original sections + * @return the converted sections + */ + public static List convert(List polySections) { + polySections.sort(new NodeSection.EdgeAngleComparator()); + + //TODO: move uniquing up to caller + List sections = extractUnique(polySections); + if (sections.size() == 1) + return sections; + + //-- find shell section index + int shellIndex = findShell(sections); + if (shellIndex < 0) { + return convertHoles(sections); + } + //-- at least one shell is present. Handle multiple ones if present + List convertedSections = new ArrayList(); + int nextShellIndex = shellIndex; + do { + nextShellIndex = convertShellAndHoles(sections, nextShellIndex, convertedSections); + } while (nextShellIndex != shellIndex); + + return convertedSections; + } + + private static int convertShellAndHoles(List sections, int shellIndex, + List convertedSections) { + NodeSection shellSection = sections.get(shellIndex); + Coordinate inVertex = shellSection.getVertex(0); + int i = next(sections, shellIndex); + NodeSection holeSection = null; + while (! sections.get(i).isShell()) { + holeSection = sections.get(i); + // Assert: holeSection.isShell() = false + Coordinate outVertex = holeSection.getVertex(1); + NodeSection ns = createSection(shellSection, inVertex, outVertex); + convertedSections.add(ns); + + inVertex = holeSection.getVertex(0); + i = next(sections, i); + } + //-- create final section for corner from last hole to shell + Coordinate outVertex = shellSection.getVertex(1); + NodeSection ns = createSection(shellSection, inVertex, outVertex); + convertedSections.add(ns); + return i; + } + + private static List convertHoles(List sections) { + List convertedSections = new ArrayList(); + NodeSection copySection = sections.get(0); + for (int i = 0; i < sections.size(); i++) { + int inext = next(sections, i); + Coordinate inVertex = sections.get(i).getVertex(0); + Coordinate outVertex = sections.get(inext).getVertex(1); + NodeSection ns = createSection(copySection, inVertex, outVertex); + convertedSections.add(ns); + } + return convertedSections; + } + + private static NodeSection createSection(NodeSection ns, Coordinate v0, Coordinate v1) { + return new NodeSection(ns.isA(), + Dimension.A, ns.id(), 0, ns.getPolygonal(), + ns.isNodeAtVertex(), + v0, ns.nodePt(), v1); + } + + private static List extractUnique(List sections) { + List uniqueSections = new ArrayList(); + NodeSection lastUnique = sections.get(0); + uniqueSections.add(lastUnique); + for (NodeSection ns : sections) { + if (0 != lastUnique.compareTo(ns)) { + uniqueSections.add(ns); + lastUnique = ns; + } + } + return uniqueSections; + } + + private static int next(List ns, int i) { + int next = i + 1; + if (next >= ns.size()) + next = 0; + return next; + } + + private static int findShell(List polySections) { + for (int i = 0; i < polySections.size(); i++) { + if (polySections.get(i).isShell()) + return i; + } + return -1; + } +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateEdge.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateEdge.java new file mode 100644 index 0000000000..e9cd8f7113 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateEdge.java @@ -0,0 +1,366 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import java.util.List; + +import org.locationtech.jts.algorithm.PolygonNodeTopology; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Location; +import org.locationtech.jts.geom.Position; +import org.locationtech.jts.io.WKTWriter; +import org.locationtech.jts.util.Assert; + +class RelateEdge { + + public static final boolean IS_FORWARD = true; + public static final boolean IS_REVERSE = false; + + public static RelateEdge create(RelateNode node, Coordinate dirPt, boolean isA, int dim, boolean isForward) { + if (dim == Dimension.A) + //-- create an area edge + return new RelateEdge(node, dirPt, isA, isForward); + //-- create line edge + return new RelateEdge(node, dirPt, isA); + } + + public static int findKnownEdgeIndex(List edges, boolean isA) { + for (int i = 0; i < edges.size(); i++) { + RelateEdge e = edges.get(i); + if (e.isKnown(isA)) + return i; + } + return -1; + } + + public static void setAreaInterior(List edges, boolean isA) { + for (RelateEdge e : edges) { + e.setAreaInterior(isA); + } + } + + /** + * The dimension of an input geometry which is not known + */ + public static final int DIM_UNKNOWN = -1; + + /** + * Indicates that the location is currently unknown + */ + private static int LOC_UNKNOWN = Location.NONE; + + private static boolean isKnown(int loc) { + return loc != LOC_UNKNOWN; + } + + private RelateNode node; + private Coordinate dirPt; + + private int aDim = DIM_UNKNOWN; + private int aLocLeft = LOC_UNKNOWN; + private int aLocRight = LOC_UNKNOWN; + private int aLocLine = LOC_UNKNOWN; + + private int bDim = DIM_UNKNOWN; + private int bLocLeft = LOC_UNKNOWN; + private int bLocRight = LOC_UNKNOWN; + private int bLocLine = LOC_UNKNOWN; + + /* + private int aDim = DIM_UNKNOWN; + private int aLocLeft = Location.EXTERIOR; + private int aLocRight = Location.EXTERIOR; + private int aLocLine = Location.EXTERIOR; + + private int bDim = DIM_UNKNOWN; + private int bLocLeft = Location.EXTERIOR; + private int bLocRight = Location.EXTERIOR; + private int bLocLine = Location.EXTERIOR; + */ + + public RelateEdge(RelateNode node, Coordinate pt, boolean isA, boolean isForward) { + this.node = node; + this.dirPt = pt; + setLocationsArea(isA, isForward); + } + + public RelateEdge(RelateNode node, Coordinate pt, boolean isA) { + this.node = node; + this.dirPt = pt; + setLocationsLine(isA); + } + + public RelateEdge(RelateNode node, Coordinate pt, boolean isA, int locLeft, int locRight, int locLine) { + this.node = node; + this.dirPt = pt; + setLocations(isA, locLeft, locRight, locLine); + } + + private void setLocations(boolean isA, int locLeft, int locRight, int locLine) { + if (isA) { + aDim = 2; + aLocLeft = locLeft; + aLocRight = locRight; + aLocLine = locLine; + } + else { + bDim = 2; + bLocLeft = locLeft; + bLocRight = locRight; + bLocLine = locLine; + } + } + + private void setLocationsLine(boolean isA) { + if (isA) { + aDim = 1; + aLocLeft = Location.EXTERIOR; + aLocRight = Location.EXTERIOR; + aLocLine = Location.INTERIOR; + } + else { + bDim = 1; + bLocLeft = Location.EXTERIOR; + bLocRight = Location.EXTERIOR; + bLocLine = Location.INTERIOR; + } + } + + private void setLocationsArea(boolean isA, boolean isForward) { + int locLeft = isForward ? Location.EXTERIOR : Location.INTERIOR; + int locRight = isForward ? Location.INTERIOR : Location.EXTERIOR; + if (isA) { + aDim = 2; + aLocLeft = locLeft; + aLocRight = locRight; + aLocLine = Location.BOUNDARY; + } + else { + bDim = 2; + bLocLeft = locLeft; + bLocRight = locRight; + bLocLine = Location.BOUNDARY; + } + } + + public int compareToEdge(Coordinate edgeDirPt) { + return PolygonNodeTopology.compareAngle(node.getCoordinate(), this.dirPt, edgeDirPt); + } + + public void merge(boolean isA, Coordinate dirPt, int dim, boolean isForward) { + int locEdge = Location.INTERIOR; + int locLeft = Location.EXTERIOR; + int locRight = Location.EXTERIOR; + if (dim == Dimension.A) { + locEdge = Location.BOUNDARY; + locLeft = isForward ? Location.EXTERIOR : Location.INTERIOR; + locRight = isForward ? Location.INTERIOR : Location.EXTERIOR; + } + + if (! isKnown(isA)) { + setDimension(isA, dim); + setOn(isA, locEdge); + setLeft(isA, locLeft); + setRight(isA, locRight); + return; + } + + // Assert: node-dirpt is collinear with node-pt + mergeDimEdgeLoc(isA, locEdge); + mergeSideLocation(isA, Position.LEFT, locLeft); + mergeSideLocation(isA, Position.RIGHT, locRight); + } + + /** + * Area edges override Line edges. + * Merging edges of same dimension is a no-op for + * the dimension and on location. + * But merging an area edge into a line edge + * sets the dimension to A and the location to BOUNDARY. + * + * @param isA + * @param locEdge + */ + private void mergeDimEdgeLoc(boolean isA, int locEdge) { + //TODO: this logic needs work - ie handling A edges marked as Interior + int dim = locEdge == Location.BOUNDARY ? Dimension.A : Dimension.L; + if (dim == Dimension.A && dimension(isA) == Dimension.L) { + setDimension(isA, dim); + setOn(isA, Location.BOUNDARY); + } + } + + private void mergeSideLocation(boolean isA, int pos, int loc) { + int currLoc = location(isA, pos); + //-- INTERIOR takes precedence over EXTERIOR + if (currLoc != Location.INTERIOR) { + setLocation(isA, pos, loc); + } + } + + private void setDimension(boolean isA, int dimension) { + if (isA) { + aDim = dimension; + } + else { + bDim = dimension; + } + } + + public void setLocation(boolean isA, int pos, int loc) { + switch (pos) { + case Position.LEFT: + setLeft(isA, loc); + break; + case Position.RIGHT: + setRight(isA, loc); + break; + case Position.ON: + setOn(isA, loc); + break; + } + } + + public void setAllLocations(boolean isA, int loc) { + setLeft(isA, loc); + setRight(isA, loc); + setOn(isA, loc); + } + + public void setUnknownLocations(boolean isA, int loc) { + if (! isKnown(isA, Position.LEFT)) { + setLocation(isA, Position.LEFT, loc); + } + if (! isKnown(isA, Position.RIGHT)) { + setLocation(isA, Position.RIGHT, loc); + } + if (! isKnown(isA, Position.ON)) { + setLocation(isA, Position.ON, loc); + } + } + + private void setLeft(boolean isA, int loc) { + if (isA) { + aLocLeft = loc; + } + else { + bLocLeft = loc; + } + } + + private void setRight(boolean isA, int loc) { + if (isA) { + aLocRight = loc; + } + else { + bLocRight = loc; + } + } + + private void setOn(boolean isA, int loc) { + if (isA) { + aLocLine = loc; + } + else { + bLocLine = loc; + } + } + + public int location(boolean isA, int position) { + if (isA) { + switch (position) { + case Position.LEFT: return aLocLeft; + case Position.RIGHT: return aLocRight; + case Position.ON: return aLocLine; + } + } + else { + switch (position) { + case Position.LEFT: return bLocLeft; + case Position.RIGHT: return bLocRight; + case Position.ON: return bLocLine; + } + } + Assert.shouldNeverReachHere(); + return LOC_UNKNOWN; + } + + private int dimension(boolean isA) { + return isA ? aDim : bDim; + } + + private boolean isKnown(boolean isA) { + if (isA) + return aDim != DIM_UNKNOWN; + return bDim != DIM_UNKNOWN; + } + + private boolean isKnown(boolean isA, int pos) { + return location(isA, pos) != LOC_UNKNOWN; + } + + public boolean isInterior(boolean isA, int position) { + return location(isA, position) == Location.INTERIOR; + } + + public void setDimLocations(boolean isA, int dim, int loc) { + if (isA) { + aDim = dim; + aLocLeft = loc; + aLocRight = loc; + aLocLine = loc; + } + else { + bDim = dim; + bLocLeft = loc; + bLocRight = loc; + bLocLine = loc; + } + } + + public void setAreaInterior(boolean isA) { + if (isA) { + aLocLeft = Location.INTERIOR; + aLocRight = Location.INTERIOR; + aLocLine = Location.INTERIOR; + } + else { + bLocLeft = Location.INTERIOR; + bLocRight = Location.INTERIOR; + bLocLine = Location.INTERIOR; + } + } + + public String toString() { + return WKTWriter.toLineString(node.getCoordinate(), dirPt) + + " - " + labelString(); + } + + private String labelString() { + StringBuilder buf = new StringBuilder(); + buf.append("A:"); + buf.append(locationString(RelateGeometry.GEOM_A)); + buf.append("/B:"); + buf.append(locationString(RelateGeometry.GEOM_B)); + return buf.toString(); + } + + private String locationString(boolean isA) { + StringBuilder buf = new StringBuilder(); + buf.append(Location.toLocationSymbol(location(isA, Position.LEFT))); + buf.append(Location.toLocationSymbol(location(isA, Position.ON))); + buf.append(Location.toLocationSymbol(location(isA, Position.RIGHT))); + return buf.toString(); + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateGeometry.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateGeometry.java new file mode 100644 index 0000000000..005ddd6184 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateGeometry.java @@ -0,0 +1,378 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.locationtech.jts.algorithm.BoundaryNodeRule; +import org.locationtech.jts.algorithm.Orientation; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateArrays; +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.GeometryCollectionIterator; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.MultiLineString; +import org.locationtech.jts.geom.MultiPoint; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.geom.util.ComponentCoordinateExtracter; +import org.locationtech.jts.geom.util.PointExtracter; + +class RelateGeometry { + + public static final boolean GEOM_A = true; + public static final boolean GEOM_B = false; + + public static String name(boolean isA) { + return isA ? "A" : "B"; + } + + private Geometry geom; + private boolean isPrepared = false; + + private int geomDim = Dimension.FALSE; + private Set uniquePoints; + private BoundaryNodeRule boundaryNodeRule; + private RelatePointLocator locator; + private int elementId = 0; + private boolean hasPoints; + private boolean hasLines; + private boolean hasAreas; + private boolean isLineZeroLen; + private boolean isGeomEmpty; + + public RelateGeometry(Geometry input) { + this(input, false, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE); + } + + public RelateGeometry(Geometry input, BoundaryNodeRule bnRule) { + this(input, false, bnRule); + } + + public RelateGeometry(Geometry input, boolean isPrepared, BoundaryNodeRule bnRule) { + this.geom = input; + this.isPrepared = isPrepared; + this.boundaryNodeRule = bnRule; + //-- cache geometry metadata + isGeomEmpty = geom.isEmpty(); + isLineZeroLen = isZeroLength(geom); + geomDim = input.getDimension(); + analyzeDimensions(); + } + + private void analyzeDimensions() { + if (isGeomEmpty) { + return; + } + if (geom instanceof Point || geom instanceof MultiPoint) { + hasPoints = true; + geomDim = Dimension.P; + return; + } + if (geom instanceof LineString || geom instanceof MultiLineString) { + hasLines = true; + geomDim = Dimension.L; + return; + } + if (geom instanceof Polygon || geom instanceof MultiPolygon) { + hasAreas = true; + geomDim = Dimension.A; + return; + } + //-- analyze a (possibly mixed type) collection + Iterator geomi = new GeometryCollectionIterator(geom); + while (geomi.hasNext()) { + Geometry elem = (Geometry) geomi.next(); + if (elem.isEmpty()) + continue; + if (elem instanceof Point) { + hasPoints = true; + if (geomDim < Dimension.P) geomDim = Dimension.P; + } + if (elem instanceof LineString) { + hasLines = true; + if (geomDim < Dimension.L) geomDim = Dimension.L; + } + if (elem instanceof Polygon) { + hasAreas = true; + if (geomDim < Dimension.A) geomDim = Dimension.A; + } + } + } + + /** + * Tests if all geometry linear elements are zero-length. + * For efficiency the test avoids computing actual length. + * + * @param geom + * @return + */ + private static boolean isZeroLength(Geometry geom) { + Iterator geomi = new GeometryCollectionIterator(geom); + while (geomi.hasNext()) { + Geometry elem = (Geometry) geomi.next(); + if (elem instanceof LineString) { + if (! isZeroLength((LineString) elem)) + return false; + } + } + return true; + } + + private static boolean isZeroLength(LineString line) { + if (line.getNumPoints() >= 2) { + Coordinate p0 = line.getCoordinateN(0); + for (int i = 0 ; i < line.getNumPoints(); i++) { + Coordinate pi = line.getCoordinateN(1); + //-- most non-zero-len lines will trigger this right away + if (! p0.equals2D(pi)) + return false; + } + } + return true; + } + + + public Geometry getGeometry() { + return geom; + } + + public boolean isPrepared() { + return isPrepared; + } + + public Envelope getEnvelope() { + return geom.getEnvelopeInternal(); + } + + public int getDimension() { + return geomDim; + } + + public boolean hasDimension(int dim) { + switch (dim) { + case Dimension.P: return hasPoints; + case Dimension.L: return hasLines; + case Dimension.A: return hasAreas; + } + return false; + } + + public int getDimensionReal() { + if (isGeomEmpty) return Dimension.FALSE; + if (getDimension() == 1 && isLineZeroLen) + return Dimension.P; + if (hasAreas) return Dimension.A; + if (hasLines) return Dimension.L; + return Dimension.P; + } + + public boolean hasEdges() { + return hasLines || hasAreas; + } + + private RelatePointLocator getLocator() { + if (locator == null) + locator = new RelatePointLocator(geom, isPrepared, boundaryNodeRule); + return locator; + } + + public boolean isNodeInArea(Coordinate nodePt, Geometry parentPolygonal) { + int loc = getLocator().locateNodeWithDim(nodePt, parentPolygonal); + return loc == DimensionLocation.AREA_INTERIOR; + } + + public int locateLineEnd(Coordinate p) { + return getLocator().locateLineEnd(p); + } + + /** + * Locates a vertex of a polygon. + * + * @param pt the polygon vertex + * @return the location of the vertex + */ + public int locateAreaVertex(Coordinate pt) { + /** + * Can pass a null polygon, because the point is an exact vertex, + * which will be detected as being on the boundary of its polygon + */ + return locateNode(pt, null); + } + + public int locateNode(Coordinate pt, Geometry parentPolygonal) { + return getLocator().locateNode(pt, parentPolygonal); + } + + public int locateWithDim(Coordinate pt) { + int loc = getLocator().locateWithDim(pt); + return loc; + } + + public boolean isPointsOrPolygons() { + return geom instanceof Point + || geom instanceof MultiPoint + || geom instanceof Polygon + || geom instanceof MultiPolygon; + } + + /** + * Tests whether the geometry has polygonal topology. + * This is not the case if it is a GeometryCollection + * containing more than one polygon (since they may overlap + * or be adjacent). + * The significance is that polygonal topology allows more assumptions + * about the location of boundary vertices. + * + * @return true if the geometry has polygonal topology + */ + public boolean isPolygonal() { + //TODO: also true for a GC containing one polygonal element (and possibly some lower-dimension elements) + return geom instanceof Polygon + || geom instanceof MultiPolygon; + } + + public boolean isEmpty() { + return isGeomEmpty; + } + + public boolean hasBoundary() { + return getLocator().hasBoundary(); + } + + public Set getUniquePoints() { + //-- will be re-used in prepared mode + if (uniquePoints == null) { + uniquePoints = createUniquePoints(); + } + return uniquePoints; + } + + private Set createUniquePoints() { + //-- only called on P geometries + List pts = ComponentCoordinateExtracter.getCoordinates(geom); + Set set = new HashSet(); + set.addAll(pts); + return set; + } + + public List getEffectivePoints() { + List ptListAll = PointExtracter.getPoints(geom); + + if (getDimensionReal() <= Dimension.P) + return ptListAll; + + //-- only return Points not covered by another element + List ptList = new ArrayList(); + for (Point p : ptListAll) { + int locDim = locateWithDim(p.getCoordinate()); + if (DimensionLocation.dimension(locDim) == Dimension.P) { + ptList.add(p); + } + } + return ptList; + } + + /** + * Extract RelateSegmentStrings from the geometry which + * intersect a given envelope. + * If the envelope is null all edges are extracted. + * @param geomA + * + * @param env the envelope to extract around (may be null) + * @return a list of RelateSegmentStrings + */ + public List extractSegmentStrings(boolean isA, Envelope env) { + List segStrings = new ArrayList(); + extractSegmentStrings(isA, env, geom, segStrings); + return segStrings; + } + + private void extractSegmentStrings(boolean isA, Envelope env, Geometry geom, List segStrings) { + //-- record if parent is MultiPolygon + MultiPolygon parentPolygonal = null; + if (geom instanceof MultiPolygon) { + parentPolygonal = (MultiPolygon) geom; + } + + for (int i = 0; i < geom.getNumGeometries(); i++) { + Geometry g = geom.getGeometryN(i); + if (g instanceof GeometryCollection) { + extractSegmentStrings(isA, env, g, segStrings); + } + else { + extractSegmentStringsFromAtomic(isA, g, parentPolygonal, env, segStrings); + } + } + } + + private void extractSegmentStringsFromAtomic(boolean isA, Geometry geom, MultiPolygon parentPolygonal, Envelope env, + List segStrings) { + if (geom.isEmpty()) + return; + boolean doExtract = env == null || env.intersects(geom.getEnvelopeInternal()); + if (! doExtract) + return; + + elementId++; + if (geom instanceof LineString) { + RelateSegmentString ss = RelateSegmentString.createLine(geom.getCoordinates(), isA, elementId, this); + segStrings.add(ss); + } + else if (geom instanceof Polygon) { + Polygon poly = (Polygon) geom; + Geometry parentPoly = parentPolygonal != null ? parentPolygonal : poly; + extractRingToSegmentString(isA, poly.getExteriorRing(), 0, env, parentPoly, segStrings); + for (int i = 0; i < poly.getNumInteriorRing(); i++) { + extractRingToSegmentString(isA, poly.getInteriorRingN(i), i+1, env, parentPoly, segStrings); + } + } + } + + private void extractRingToSegmentString(boolean isA, LinearRing ring, int ringId, Envelope env, + Geometry parentPoly, List segStrings) { + if (ring.isEmpty()) + return; + if (env != null && ! env.intersects(ring.getEnvelopeInternal())) + return; + + //-- orient the points if required + boolean requireCW = ringId == 0; + Coordinate[] pts = orient(ring.getCoordinates(), requireCW); + RelateSegmentString ss = RelateSegmentString.createRing(pts, isA, elementId, ringId, parentPoly, this); + segStrings.add(ss); + } + + public static Coordinate[] orient(Coordinate[] pts, boolean orientCW) { + boolean isFlipped = orientCW == Orientation.isCCW(pts); + if (isFlipped) { + pts = pts.clone(); + CoordinateArrays.reverse(pts); + } + return pts; + } + + public String toString() { + return geom.toString(); + } + + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateMatrixPredicate.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateMatrixPredicate.java new file mode 100644 index 0000000000..1e224c0221 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateMatrixPredicate.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import org.locationtech.jts.geom.IntersectionMatrix; + +/** + * Evaluates the full relate {@link IntersectionMatrix}. + * @author mdavis + * + */ +class RelateMatrixPredicate extends IMPredicate +{ + public RelateMatrixPredicate() { + } + + public String name() { return "relateMatrix"; } + + @Override + public boolean isDetermined() { + //-- ensure entire matrix is computed + return false; + } + + @Override + public boolean valueIM() { + //-- indicates full matrix is being evaluated + return false; + + } + + /** + * Gets the current state of the IM matrix (which may only be partially complete). + * + * @return the IM matrix + */ + public IntersectionMatrix getIM() { + return intMatrix; + } + +} \ No newline at end of file diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java new file mode 100644 index 0000000000..977fd75637 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java @@ -0,0 +1,505 @@ +/* + * Copyright (c) 2024 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import static org.locationtech.jts.operation.relateng.RelateGeometry.GEOM_A; +import static org.locationtech.jts.operation.relateng.RelateGeometry.GEOM_B; + +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.locationtech.jts.algorithm.BoundaryNodeRule; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollectionIterator; +import org.locationtech.jts.geom.IntersectionMatrix; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.Location; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.geom.prep.PreparedGeometry; +import org.locationtech.jts.noding.MCIndexSegmentSetMutualIntersector; +import org.locationtech.jts.operation.relate.RelateOp; + +/** + * Computes the value of topological predicates between two geometries based on the + * Dimensionally-Extended 9-Intersection Model (DE-9IM). + * Standard and custom topological predicates are provided by {@link RelatePredicate}. + *

+ * The RelateNG algorithm has the following capabilities: + *

    + *
  1. Efficient short-circuited evaluation of topological predicates + * (including matching custom DE-9IM matrix patterns) + *
  2. Optimized repeated evaluation of predicates against a single geometry + * via cached spatial indexes (AKA "prepared mode") + *
  3. Robust computation (only point-local topology is required, + * so invalid geometry topology does not cause failures) + *
  4. {@link GeometryCollection} inputs containing mixed types and overlapping polygons + * are supported, using union semantics. + *
  5. Zero-length LineStrings are treated as being topologically identical to Points. + *
  6. Support for {@link BoundaryNodeRule}s. + *
+ * + * See {@link IntersectionMatrixPattern} for a description of DE-9IM patterns. + * + * If not specified, the standard {@link BoundaryNodeRule#MOD2_BOUNDARY_RULE} is used. + * + * RelateNG operates in 2D only; it ignores any Z ordinates. + * + * This implementation replaces {@link RelateOp} and {@link PreparedGeometry}. + * + *

FUTURE WORK

+ *
    + *
  • Support for a distance tolerance to provide "approximate" predicate evaluation + *
+ * + * + * @author Martin Davis + * + * @see RelateOp + * @see PreparedGeometry + */ +public class RelateNG +{ + + /** + * Tests whether the topological relationship between two geometries + * satisfies a topological predicate. + * + * @param a the A input geometry + * @param b the A input geometry + * @param pred the topological predicate + * @return true if the topological relationship is satisfied + */ + public static boolean relate(Geometry a, Geometry b, TopologyPredicate pred) { + RelateNG rng = new RelateNG(a, false); + return rng.evaluate(b, pred); + } + + /** + * Tests whether the topological relationship between two geometries + * satisfies a topological predicate, + * using a given {@link BoundaryNodeRule}. + * + * @param a the A input geometry + * @param b the A input geometry + * @param pred the topological predicate + * @param bnRule the Boundary Node Rule to use + * @return true if the topological relationship is satisfied + */ + public static boolean relate(Geometry a, Geometry b, TopologyPredicate pred, BoundaryNodeRule bnRule) { + RelateNG rng = new RelateNG(a, false, bnRule); + return rng.evaluate(b, pred); + } + + /** + * Tests whether the topological relationship to a geometry + * matches a DE-9IM matrix pattern. + * + * @param a the A input geometry + * @param b the A input geometry + * @param imPattern the DE-9IM pattern to match + * @return true if the geometries relationship matches the DE-9IM pattern + * + * @see IntersectionMatrixPattern + */ + public static boolean relate(Geometry a, Geometry b, String imPattern) { + RelateNG rng = new RelateNG(a, false); + return rng.evaluate(b, imPattern); + } + + /** + * Computes the DE-9IM matrix + * for the topological relationship between two geometries. + * + * @param a the A input geometry + * @param b the A input geometry + * @return the DE-9IM matrix for the topological relationship + */ + public static IntersectionMatrix relate(Geometry a, Geometry b) { + RelateNG rng = new RelateNG(a, false); + return rng.evaluate(b); + } + + /** + * Computes the DE-9IM matrix + * for the topological relationship between two geometries. + * + * @param a the A input geometry + * @param b the A input geometry + * @param bnRule the Boundary Node Rule to use + * @return the DE-9IM matrix for the relationship + */ + public static IntersectionMatrix relate(Geometry a, Geometry b, BoundaryNodeRule bnRule) { + RelateNG rng = new RelateNG(a, false, bnRule); + return rng.evaluate(b); + } + + /** + * Creates a prepared RelateNG instance to optimize the + * evaluation of relationships against a single geometry. + * + * @param a the A input geometry + * @return a prepared instance + */ + public static RelateNG prepare(Geometry a) { + return new RelateNG(a, true); + } + + /** + * Creates a prepared RelateNG instance to optimize the + * computation of predicates against a single geometry, + * using a given {@link BoundaryNodeRule}. + * + * @param a the A input geometry + * @param bnRule the required BoundaryNodeRule + * @return a prepared instance + */ + public static RelateNG prepare(Geometry a, BoundaryNodeRule bnRule) { + return new RelateNG(a, true, bnRule); + } + + private BoundaryNodeRule boundaryNodeRule; + private RelateGeometry geomA; + private MCIndexSegmentSetMutualIntersector edgeMutualInt; + + private RelateNG(Geometry inputA, boolean isPrepared) { + this(inputA, isPrepared, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE); + } + + private RelateNG(Geometry inputA, boolean isPrepared, BoundaryNodeRule bnRule) { + this.boundaryNodeRule = bnRule; + geomA = new RelateGeometry(inputA, isPrepared, boundaryNodeRule); + } + + /** + * Computes the DE-9IM matrix for the topological relationship to a geometry. + * + * @param b the B geometry to test against + * @return the DE-9IM matrix + */ + public IntersectionMatrix evaluate(Geometry b) { + RelateMatrixPredicate rel = new RelateMatrixPredicate(); + evaluate(b, rel); + return rel.getIM(); + } + + /** + * Tests whether the topological relationship to a geometry + * matches a DE-9IM matrix pattern. + * + * @param b the B geometry to test against + * @param imPattern the DE-9IM pattern to match + * @return true if the geometries' topological relationship matches the DE-9IM pattern + * + * @see IntersectionMatrixPattern + */ + public boolean evaluate(Geometry b, String imPattern) { + return evaluate(b, RelatePredicate.matches(imPattern)); + } + + /** + * Tests whether the topological relationship to a geometry + * satisfies a topology predicate. + * + * @param b the B geometry to test against + * @param predicate the topological predicate + * @return true if the predicate is satisfied + */ + public boolean evaluate(Geometry b, TopologyPredicate predicate) { + + RelateGeometry geomB = new RelateGeometry(b, boundaryNodeRule); + + if (geomA.isEmpty() && geomB.isEmpty()) { + //TODO: what if predicate is disjoint? Perhaps use result on disjoint envs? + return finishValue(predicate); + } + int dimA = geomA.getDimensionReal(); + int dimB = geomB.getDimensionReal(); + + //-- check if predicate is determined by dimension or envelope + predicate.init(dimA, dimB); + if (predicate.isKnown()) + return finishValue(predicate); + + predicate.init(geomA.getEnvelope(), geomB.getEnvelope()); + if (predicate.isKnown()) + return finishValue(predicate); + + TopologyComputer topoComputer = new TopologyComputer(predicate, geomA, geomB); + + //-- optimized P/P evaluation + if (dimA == Dimension.P && dimB == Dimension.P) { + computePP(geomB, topoComputer); + topoComputer.finish(); + return topoComputer.getResult(); + } + + //-- test points against (potentially) indexed geometry first + computeAtPoints(geomB, GEOM_B, geomA, topoComputer); + if (topoComputer.isResultKnown()) { + return topoComputer.getResult(); + } + computeAtPoints(geomA, GEOM_A, geomB, topoComputer); + if (topoComputer.isResultKnown()) { + return topoComputer.getResult(); + } + + if (geomA.hasEdges() && geomB.hasEdges()) { + computeAtEdges(geomB, topoComputer); + } + + //-- after all processing, set remaining unknown values in IM + topoComputer.finish(); + return topoComputer.getResult(); + } + + private boolean finishValue(TopologyPredicate predicate) { + predicate.finish(); + return predicate.value(); + } + + /** + * An optimized algorithm for evaluating P/P cases. + * It tests one point set against the other. + * + * @param geomB + * @param topoComputer + */ + private void computePP(RelateGeometry geomB, TopologyComputer topoComputer) { + Set ptsA = geomA.getUniquePoints(); + //TODO: only query points in interaction extent? + Set ptsB = geomB.getUniquePoints(); + + int numBinA = 0; + for (Coordinate ptB : ptsB) { + if (ptsA.contains(ptB)) { + numBinA++; + topoComputer.addPointOnPointInterior(ptB); + } + else { + topoComputer.addPointOnPointExterior(GEOM_B, ptB); + } + if (topoComputer.isResultKnown()) { + return; + } + } + /** + * If number of matched B points is less than size of A, + * there must be at least one A point in the exterior of B + */ + if (numBinA < ptsA.size()) { + //TODO: determine actual exterior point? + topoComputer.addPointOnPointExterior(GEOM_A, null); + } + } + + private void computeAtPoints(RelateGeometry geom, boolean isA, + RelateGeometry geomTarget, TopologyComputer topoComputer) { + + boolean isResultKnown = false; + isResultKnown = computePoints(geom, isA, geomTarget, topoComputer); + if (isResultKnown) + return; + + /** + * Performance optimization: only check points against target + * if it has areas OR if the predicate requires checking for + * exterior interaction. + * In particular, this avoids testing line ends against lines + * for the intersects predicate. + */ + boolean checkDisjointPoints = geomTarget.hasDimension(Dimension.A) + || topoComputer.isExteriorCheckRequired(isA); + if (! checkDisjointPoints) + return; + + isResultKnown = computeLineEnds(geom, isA, geomTarget, topoComputer); + if (isResultKnown) + return; + + computeAreaVertex(geom, isA, geomTarget, topoComputer); + } + + private boolean computePoints(RelateGeometry geom, boolean isA, RelateGeometry geomTarget, + TopologyComputer topoComputer) { + if (! geom.hasDimension(Dimension.P)) { + return false; + } + + List points = geom.getEffectivePoints(); + for (Point point : points) { + //TODO: exit when all possible target locations (E,I,B) have been found? + if (point.isEmpty()) + continue; + + Coordinate pt = point.getCoordinate(); + computePoint(isA, pt, geomTarget, topoComputer); + if (topoComputer.isResultKnown()) { + return true; + } + } + return false; + } + + private void computePoint(boolean isA, Coordinate pt, RelateGeometry geomTarget, TopologyComputer topoComputer) { + int locDimTarget = geomTarget.locateWithDim(pt); + int locTarget = DimensionLocation.location(locDimTarget); + int dimTarget = DimensionLocation.dimension(locDimTarget, topoComputer.getDimension(! isA)); + topoComputer.addPointOnGeometry(isA, locTarget, dimTarget, pt); + } + + private boolean computeLineEnds(RelateGeometry geom, boolean isA, RelateGeometry geomTarget, + TopologyComputer topoComputer) { + if (! geom.hasDimension(Dimension.L)) { + return false; + } + + boolean hasExteriorIntersection = false; + Iterator geomi = new GeometryCollectionIterator(geom.getGeometry()); + while (geomi.hasNext()) { + Geometry elem = (Geometry) geomi.next(); + if (elem.isEmpty()) + continue; + + if (elem instanceof LineString) { + //-- once an intersection with target exterior is recorded, skip further known-exterior points + if (hasExteriorIntersection + && elem.getEnvelopeInternal().disjoint(geomTarget.getEnvelope())) + continue; + + LineString line = (LineString) elem; + //TODO: add optimzation to skip disjoint elements once exterior point found + Coordinate e0 = line.getCoordinateN(0); + hasExteriorIntersection |= computeLineEnd(geom, isA, e0, geomTarget, topoComputer); + if (topoComputer.isResultKnown()) { + return true; + } + + if (! line.isClosed()) { + Coordinate e1 = line.getCoordinateN(line.getNumPoints() - 1); + hasExteriorIntersection |= computeLineEnd(geom, isA, e1, geomTarget, topoComputer); + if (topoComputer.isResultKnown()) { + return true; + } + } + //TODO: break when all possible locations have been found? + } + } + return false; + } + + private boolean computeLineEnd(RelateGeometry geom, boolean isA, Coordinate pt, + RelateGeometry geomTarget, TopologyComputer topoComputer) { + int locLineEnd = geom.locateLineEnd(pt); + int locDimTarget = geomTarget.locateWithDim(pt); + int locTarget = DimensionLocation.location(locDimTarget); + int dimTarget = DimensionLocation.dimension(locDimTarget, topoComputer.getDimension(! isA)); + topoComputer.addLineEndOnGeometry(isA, locLineEnd, locTarget, dimTarget, pt); + return locTarget == Location.EXTERIOR; + } + + private boolean computeAreaVertex(RelateGeometry geom, boolean isA, RelateGeometry geomTarget, TopologyComputer topoComputer) { + if (! geom.hasDimension(Dimension.A)) { + return false; + } + //-- evaluate for line and area targets only, since points are handled in the reverse direction + if (geomTarget.getDimension() < Dimension.L) + return false; + + boolean hasExteriorIntersection = false; + Iterator geomi = new GeometryCollectionIterator(geom.getGeometry()); + while (geomi.hasNext()) { + Geometry elem = (Geometry) geomi.next(); + if (elem.isEmpty()) + continue; + + if (elem instanceof Polygon) { + //-- once an intersection with target exterior is recorded, skip further known-exterior points + if (hasExteriorIntersection + && elem.getEnvelopeInternal().disjoint(geomTarget.getEnvelope())) + continue; + + Polygon poly = (Polygon) elem; + hasExteriorIntersection |= computeAreaVertex(geom, isA, poly.getExteriorRing(), geomTarget, topoComputer); + if (topoComputer.isResultKnown()) { + return true; + } + for (int j = 0; j < poly.getNumInteriorRing(); j++) { + hasExteriorIntersection |= computeAreaVertex(geom, isA, poly.getInteriorRingN(j), geomTarget, topoComputer); + if (topoComputer.isResultKnown()) { + return true; + } + } + } + } + return false; + } + + private boolean computeAreaVertex(RelateGeometry geom, boolean isA, LinearRing ring, RelateGeometry geomTarget, TopologyComputer topoComputer) { + //TODO: use extremal (highest) point to ensure one is on boundary of polygon cluster + Coordinate pt = ring.getCoordinate(); + + int locArea = geom.locateAreaVertex(pt); + int locDimTarget = geomTarget.locateWithDim(pt); + int locTarget = DimensionLocation.location(locDimTarget); + int dimTarget = DimensionLocation.dimension(locDimTarget, topoComputer.getDimension(! isA)); + topoComputer.addAreaVertex(isA, locArea, locTarget, dimTarget, pt); + return locTarget == Location.EXTERIOR; + } + + private void computeAtEdges(RelateGeometry geomB, TopologyComputer topoComputer) { + Envelope envInt = geomA.getEnvelope().intersection(geomB.getEnvelope()); + if (envInt.isNull()) + return; + + List edgesB = geomB.extractSegmentStrings(GEOM_B, envInt); + EdgeSegmentIntersector intersector = new EdgeSegmentIntersector(topoComputer); + + if (topoComputer.isSelfNodingRequired()) { + computeEdgesAll(edgesB, envInt, intersector); + } + else { + computeEdgesMutual(edgesB, envInt, intersector); + } + if (topoComputer.isResultKnown()) { + return; + } + + topoComputer.evaluateNodes(); + } + + private void computeEdgesAll(List edgesB, Envelope envInt, EdgeSegmentIntersector intersector) { + //TODO: find a way to reuse prepared index? + List edgesA = geomA.extractSegmentStrings(GEOM_A, envInt); + + EdgeSetIntersector edgeInt = new EdgeSetIntersector(edgesA, edgesB, envInt); + edgeInt.process(intersector); + } + + private void computeEdgesMutual(List edgesB, Envelope envInt, EdgeSegmentIntersector intersector) { + //-- in prepared mode the A edge index is reused + if (edgeMutualInt == null) { + Envelope envExtract = geomA.isPrepared() ? null : envInt; + List edgesA = geomA.extractSegmentStrings(GEOM_A, envExtract); + edgeMutualInt = new MCIndexSegmentSetMutualIntersector(edgesA, envExtract); + } + + edgeMutualInt.process(edgesB, intersector); + } + + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNode.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNode.java new file mode 100644 index 0000000000..92bfd8cbe2 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNode.java @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import java.util.ArrayList; +import java.util.List; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Location; +import org.locationtech.jts.geom.Position; +import org.locationtech.jts.io.WKTWriter; +import org.locationtech.jts.util.Assert; +import org.locationtech.jts.util.Debug; + +class RelateNode { + + private Coordinate nodePt; + + /** + * A list of the edges around the node in CCW order, + * ordered by their CCW angle with the positive X-axis. + */ + private ArrayList edges = new ArrayList(); + + public RelateNode(Coordinate pt) { + this.nodePt = pt; + } + + public Coordinate getCoordinate() { + return nodePt; + } + + public List getEdges() { + return edges; + } + + + public void addEdges(List nss) { + for (NodeSection ns : nss) { + addEdges(ns); + } + } + + public void addEdges(NodeSection ns) { + //Debug.println("Adding NS: " + ns); + switch (ns.dimension()) { + case Dimension.L: + addLineEdge(ns.isA(), ns.getVertex(0)); + addLineEdge(ns.isA(), ns.getVertex(1)); + break; + case Dimension.A: + //-- assumes node edges have CW orientation (as per JTS norm) + //-- entering edge - interior on L + RelateEdge e0 = addAreaEdge(ns.isA(), ns.getVertex(0), false); + //-- exiting edge - interior on R + RelateEdge e1 = addAreaEdge(ns.isA(), ns.getVertex(1), true); + + int index0 = edges.indexOf(e0); + int index1 = edges.indexOf(e1); + updateEdgesInArea(ns.isA(), index0, index1); + updateIfAreaPrev(ns.isA(), index0); + updateIfAreaNext(ns.isA(), index1); + } + } + + private void updateEdgesInArea(boolean isA, int indexFrom, int indexTo) { + int index = nextIndex(edges, indexFrom); + while (index != indexTo) { + RelateEdge edge = edges.get(index); + edge.setAreaInterior(isA); + index = nextIndex(edges, index); + } + } + + private void updateIfAreaPrev(boolean isA, int index) { + int indexPrev = prevIndex(edges, index); + RelateEdge edgePrev = edges.get(indexPrev); + if (edgePrev.isInterior(isA, Position.LEFT)) { + RelateEdge edge = edges.get(index); + edge.setAreaInterior(isA); + } + } + + private void updateIfAreaNext(boolean isA, int index) { + int indexNext = nextIndex(edges, index); + RelateEdge edgeNext = edges.get(indexNext); + if (edgeNext.isInterior(isA, Position.RIGHT)) { + RelateEdge edge = edges.get(index); + edge.setAreaInterior(isA); + } + } + + private RelateEdge addLineEdge(boolean isA, Coordinate dirPt) { + return addEdge(isA, dirPt, Dimension.L, false); + } + + private RelateEdge addAreaEdge(boolean isA, Coordinate dirPt, boolean isForward) { + return addEdge(isA, dirPt, Dimension.A, isForward); + } + + /** + * Adds or merges an edge to the node. + * + * @param isA + * @param dirPt + * @param dim dimension of the geometry element containing the edge + * @param isForward the direction of the edge + * + * @return the created or merged edge for this point + */ + private RelateEdge addEdge(boolean isA, Coordinate dirPt, int dim, boolean isForward) { + //-- check for well-formed edge - skip null or zero-len input + if (dirPt == null) + return null; + if (nodePt.equals2D(dirPt)) + return null; + + int insertIndex = -1; + for (int i = 0; i < edges.size(); i++) { + RelateEdge e = edges.get(i); + int comp = e.compareToEdge(dirPt); + if (comp == 0) { + e.merge(isA, dirPt, dim, isForward); + return e; + } + if (comp == 1 ) { + //-- found further edge, so insert a new edge at this position + insertIndex = i; + break; + } + } + //-- add a new edge + RelateEdge e = RelateEdge.create(this, dirPt, isA, dim, isForward); + if (insertIndex < 0) { + //-- add edge at end of list + edges.add(e); + } + else { + //-- add edge before higher edge found + edges.add(insertIndex, e); + } + return e; + } + + /** + * Computes the final topology for the edges around this node. + * Although nodes lie on the boundary of areas or the interior of lines, + * in a mixed GC they may also lie in the interior of an area. + * This changes the locations of the sides and line to Interior. + * + * @param isAreaInteriorA true if the node is in the interior of A + * @param isAreaInteriorB true if the node is in the interior of B + */ + public void finish(boolean isAreaInteriorA, boolean isAreaInteriorB) { + +//Debug.println("finish Node."); +//Debug.println("Before: " + this); + + finishNode(RelateGeometry.GEOM_A, isAreaInteriorA); + finishNode(RelateGeometry.GEOM_B, isAreaInteriorB); +//Debug.println("After: " + this); + } + + private void finishNode(boolean isA, boolean isAreaInterior) { + if (isAreaInterior) { + RelateEdge.setAreaInterior(edges, isA); + } + else { + int startIndex = RelateEdge.findKnownEdgeIndex(edges, isA); + //-- only interacting nodes are finished, so this should never happen + //Assert.isTrue(startIndex >= 0l, "Node at "+ nodePt + "does not have AB interaction"); + propagateSideLocations(isA, startIndex); + } + } + + private void propagateSideLocations(boolean isA, int startIndex) { + int currLoc = edges.get(startIndex).location(isA, Position.LEFT); + //-- edges are stored in CCW order + int index = nextIndex(edges, startIndex); + while (index != startIndex) { + RelateEdge e = edges.get(index); + e.setUnknownLocations(isA, currLoc); + currLoc = e.location(isA, Position.LEFT); + index = nextIndex(edges, index); + } + } + + private static int prevIndex(ArrayList list, int index) { + if (index > 0) + return index - 1; + //-- index == 0 + return list.size() - 1; + } + + private static int nextIndex(List list, int i) { + if (i >= list.size() - 1) { + return 0; + } + return i + 1; + } + + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append("Node[" + WKTWriter.toPoint(nodePt) + "]:"); + buf.append("\n"); + for (RelateEdge e : edges) { + buf.append(e.toString()); + buf.append("\n"); + } + return buf.toString(); + } + + public boolean hasExteriorEdge(boolean isA) { + for (RelateEdge e : edges) { + if (Location.EXTERIOR == e.location(isA, Position.LEFT) + || Location.EXTERIOR == e.location(isA, Position.RIGHT)) { + return true; + } + } + return false; + } +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePointLocator.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePointLocator.java new file mode 100644 index 0000000000..35ae78f2e9 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePointLocator.java @@ -0,0 +1,316 @@ +/* + * Copyright (c) 2024 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.locationtech.jts.algorithm.BoundaryNodeRule; +import org.locationtech.jts.algorithm.PointLocation; +import org.locationtech.jts.algorithm.locate.IndexedPointInAreaLocator; +import org.locationtech.jts.algorithm.locate.PointOnGeometryLocator; +import org.locationtech.jts.algorithm.locate.SimplePointInAreaLocator; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.Location; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; + +/** + * Locates a point on a geometry, including mixed-type collections. + * The dimension of the containing geometry element is also determined. + * GeometryCollections are handled with union semantics; + * i.e. the location of a point is that location of that point + * on the union of the elements of the collection. + *

+ * Union semantics for GeometryCollections has the following behaviours: + *

    + *
  1. For a mixed-dimension (heterogeneous) collection + * a point may lie on two geometry elements with different dimensions. + * In this case the location on the largest-dimension element is reported. + *
  2. For a collection with overlapping or adjacent polygons, + * points on polygon element boundaries may lie in the effective interior + * of the collection geometry. + *
+ * Prepared mode is supported via cached spatial indexes. + * + * @author Martin Davis + * + */ +class RelatePointLocator { + + private Geometry geom; + private boolean isPrepared = false; + private BoundaryNodeRule boundaryRule; + private AdjacentEdgeLocator adjEdgeLocator; + private Set points; + private List lines; + private List polygons; + private PointOnGeometryLocator[] polyLocator; + private LinearBoundary lineBoundary; + private boolean isEmpty; + + public RelatePointLocator(Geometry geom) { + this(geom, false, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE); + } + + public RelatePointLocator(Geometry geom, boolean isPrepared, BoundaryNodeRule bnRule) { + this.geom = geom; + this.isPrepared = isPrepared; + this.boundaryRule = bnRule; + init(geom); + } + + private void init(Geometry geom) { + //-- cache empty status, since may be checked many times + isEmpty = geom.isEmpty(); + extractElements(geom); + + if (lines != null) { + lineBoundary = new LinearBoundary(lines, boundaryRule); + } + + if (polygons != null) { + polyLocator = isPrepared + ? new IndexedPointInAreaLocator[polygons.size()] + : new SimplePointInAreaLocator[polygons.size()]; + } + } + + public boolean hasBoundary() { + return lineBoundary.hasBoundary(); + } + + private void extractElements(Geometry geom) { + if (geom.isEmpty()) + return; + + if (geom instanceof Point) { + addPoint((Point) geom); + } + else if (geom instanceof LineString) { + addLine((LineString) geom); + } + else if (geom instanceof Polygon + || geom instanceof MultiPolygon) { + addPolygonal(geom); + } + else if (geom instanceof GeometryCollection){ + for (int i = 0; i < geom.getNumGeometries(); i++) { + Geometry g = geom.getGeometryN(i); + extractElements(g); + } + } + } + + private void addPoint(Point pt) { + if (points == null) { + points = new HashSet(); + } + points.add(pt.getCoordinate()); + } + + private void addLine(LineString line) { + if (lines == null) { + lines = new ArrayList(); + } + lines.add(line); + } + + private void addPolygonal(Geometry polygonal) { + if (polygons == null) { + polygons = new ArrayList(); + } + polygons.add(polygonal); + } + + public int locate(Coordinate p) { + return DimensionLocation.location(locateWithDim(p)); + } + + public int locateLineEnd(Coordinate p) { + return lineBoundary.isBoundary(p) ? Location.BOUNDARY : Location.INTERIOR; + } + + /** + * Locates a point which is known to be a node of the geometry + * (i.e. a point or on an edge). + * + * @param p the node point to locate + * @param polygonal + * @return the location of the node point + */ + public int locateNode(Coordinate p, Geometry parentPolygonal) { + return DimensionLocation.location(locateNodeWithDim(p, parentPolygonal)); + } + + public int locateNodeWithDim(Coordinate p, Geometry parentPolygonal) { + return locateWithDim(p, true, parentPolygonal); + } + + /** + * Computes the topological location ({@link Location}) of a single point + * in a Geometry, as well as the dimension of the geometry element the point + * is located in (if not in the Exterior). + * It handles both single-element and multi-element Geometries. + * The algorithm for multi-part Geometries + * takes into account the SFS Boundary Determination Rule. + * + * @param p the point to locate + * @return the {@link Location} of the point relative to the input Geometry + */ + public int locateWithDim(Coordinate p) { + return locateWithDim(p, false, null); + } + + /** + * Computes the topological location ({@link Location}) of a single point + * in a Geometry, as well as the dimension of the geometry element the point + * is located in (if not in the Exterior). + * It handles both single-element and multi-element Geometries. + * The algorithm for multi-part Geometries + * takes into account the SFS Boundary Determination Rule. + * + * @param p the coordinate to locate + * @param isNode whether the coordinate is a node (on an edge) of the geometry + * @param polygon + * @return the {@link Location} of the point relative to the input Geometry + */ + private int locateWithDim(Coordinate p, boolean isNode, Geometry parentPolygonal) + { + if (isEmpty) return DimensionLocation.EXTERIOR; + + /** + * In a polygonal geometry a node must be on the boundary. + * (This is not the case for a mixed collection, since + * the node may be in the interior of a polygon.) + */ + if (isNode && (geom instanceof Polygon || geom instanceof MultiPolygon)) + return DimensionLocation.AREA_BOUNDARY; + + int dimLoc = computeDimLocation(p, isNode, parentPolygonal); + return dimLoc; + } + + private int computeDimLocation(Coordinate p, boolean isNode, Geometry parentPolygonal) { + //-- check dimensions in order of precedence + if (polygons != null) { + int locPoly = locateOnPolygons(p, isNode, parentPolygonal); + if (locPoly != Location.EXTERIOR) + return DimensionLocation.locationArea(locPoly); + } + if (lines != null) { + int locLine = locateOnLines(p, isNode); + if (locLine != Location.EXTERIOR) + return DimensionLocation.locationLine(locLine); + } + if (points != null) { + int locPt = locateOnPoints(p); + if (locPt != Location.EXTERIOR) + return DimensionLocation.locationPoint(locPt); + } + return DimensionLocation.EXTERIOR; + } + + private int locateOnPoints(Coordinate p) { + if (points.contains(p)) { + return Location.INTERIOR; + } + return Location.EXTERIOR; + } + + private int locateOnLines(Coordinate p, boolean isNode) { + if (lineBoundary != null + && lineBoundary.isBoundary(p)) { + return Location.BOUNDARY; + } + //-- must be on line, in interior + if (isNode) + return Location.INTERIOR; + + //TODO: index the lines + for (LineString line : lines) { + //-- have to check every line, since any/all may contain point + int loc = locateOnLine(p, isNode, line); + if (loc != Location.EXTERIOR) + return loc; + //TODO: minor optimization - some BoundaryNodeRules can short-circuit + } + return Location.EXTERIOR; + } + + private int locateOnLine(Coordinate p, boolean isNode, LineString l) + { + // bounding-box check + if (! l.getEnvelopeInternal().intersects(p)) + return Location.EXTERIOR; + + CoordinateSequence seq = l.getCoordinateSequence(); + if (PointLocation.isOnLine(p, seq)) { + return Location.INTERIOR; + } + return Location.EXTERIOR; + } + + private int locateOnPolygons(Coordinate p, boolean isNode, Geometry parentPolygonal) { + int numBdy = 0; + //TODO: use a spatial index on the polygons + for (int i = 0; i < polygons.size(); i++) { + int loc = locateOnPolygonal(p, isNode, parentPolygonal, i); + if (loc == Location.INTERIOR) { + return Location.INTERIOR; + } + if (loc == Location.BOUNDARY) { + numBdy += 1; + } + } + if (numBdy == 1) { + return Location.BOUNDARY; + } + //-- check for point lying on adjacent boundaries + else if (numBdy > 1) { + if (adjEdgeLocator == null) { + adjEdgeLocator = new AdjacentEdgeLocator(geom); + } + return adjEdgeLocator.locate(p); + } + return Location.EXTERIOR; + } + + private int locateOnPolygonal(Coordinate p, boolean isNode, Geometry parentPolygonal, int index) { + Geometry polygonal = polygons.get(index); + if (isNode && parentPolygonal == polygonal) { + return Location.BOUNDARY; + } + PointOnGeometryLocator locator = getLocator(index); + return locator.locate(p); + } + + private PointOnGeometryLocator getLocator(int index) { + PointOnGeometryLocator locator = polyLocator[index]; + if (locator == null) { + Geometry polygonal = polygons.get(index); + locator = isPrepared + ? new IndexedPointInAreaLocator(polygonal) + : new SimplePointInAreaLocator(polygonal); + polyLocator[index] = locator; + } + return locator; + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePredicate.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePredicate.java new file mode 100644 index 0000000000..6eef9ba41f --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePredicate.java @@ -0,0 +1,596 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Location; + +/** + * Creates predicate instances for evaluating OGC-standard named topological relationships. + * Predicates can be evaluated for geometries using {@link RelateNG}. + * + * @author Martin Davis + * + */ +public interface RelatePredicate { + + /** + * Creates a predicate to determine whether two geometries intersect. + *

+ * The intersects predicate has the following equivalent definitions: + *

    + *
  • The two geometries have at least one point in common + *
  • The DE-9IM Intersection Matrix for the two geometries matches + * at least one of the patterns + *
      + *
    • [T********] + *
    • [*T*******] + *
    • [***T*****] + *
    • [****T****] + *
    + *
  • disjoint() = false + *
    (intersects is the inverse of disjoint) + *
+ * + *@return the predicate instance + * + * @see #disjoint() + */ + public static TopologyPredicate intersects() { + return new BasicPredicate() { + + public String name() { return "intersects"; } + + @Override + public boolean requiresSelfNoding() { + //-- self-noding is not required to check for a simple interaction + return false; + } + + @Override + public boolean requiresExteriorCheck(boolean isSourceA) { + //-- intersects only requires testing interaction + return false; + } + + @Override + public void init(Envelope envA, Envelope envB) { + require(envA.intersects(envB)); + } + + @Override + public void updateDimension(int locA, int locB, int dimension) { + setValueIf(true, isIntersection(locA, locB)); + } + + @Override + public void finish() { + //-- if no intersecting locations were found + setValue(false); + } + + }; + } + + /** + * Creates a predicate to determine whether two geometries are disjoint. + *

+ * The disjoint predicate has the following equivalent definitions: + *

    + *
  • The two geometries have no point in common + *
  • The DE-9IM Intersection Matrix for the two geometries matches + * [FF*FF****] + *
  • intersects() = false + *
    (disjoint is the inverse of intersects) + *
+ * + *@return the predicate instance + * + * @see #intersects() + */ + public static TopologyPredicate disjoint() { + return new BasicPredicate() { + + public String name() { return "disjoint"; } + + @Override + public boolean requiresSelfNoding() { + //-- self-noding is not required to check for a simple interaction + return false; + } + + @Override + public boolean requiresExteriorCheck(boolean isSourceA) { + //-- disjoint only requires testing interaction + return false; + } + + @Override + public void init(Envelope envA, Envelope envB) { + setValueIf(true, envA.disjoint(envB)); + } + + @Override + public void updateDimension(int locA, int locB, int dimension) { + setValueIf(false, isIntersection(locA, locB)); + } + + @Override + public void finish() { + //-- if no intersecting locations were found + setValue(true); + } + + }; + } + + /** + * Creates a predicate to determine whether a geometry contains another geometry. + *

+ * The contains predicate has the following equivalent definitions: + *

    + *
  • Every point of the other geometry is a point of this geometry, + * and the interiors of the two geometries have at least one point in common. + *
  • The DE-9IM Intersection Matrix for the two geometries matches + * the pattern + * [T*****FF*] + *
  • within(B, A) = true + *
    (contains is the converse of {@link #within} ) + *
+ * An implication of the definition is that "Geometries do not + * contain their boundary". In other words, if a geometry A is a subset of + * the points in the boundary of a geometry B, B.contains(A) = false. + * (As a concrete example, take A to be a LineString which lies in the boundary of a Polygon B.) + * For a predicate with similar behavior but avoiding + * this subtle limitation, see {@link #covers}. + * + *@return the predicate instance + * + * @see #within() + */ + public static TopologyPredicate contains() { + return new IMPredicate() { + + public String name() { return "contains"; } + + @Override + public boolean requiresExteriorCheck(boolean isSourceA) { + //-- only need to check B against Exterior of A + return isSourceA == RelateGeometry.GEOM_B; + } + + @Override + public void init(int dimA, int dimB) { + super.init(dimA, dimB); + require( isDimsCompatibleWithCovers(dimA, dimB) ); + } + + @Override + public void init(Envelope envA, Envelope envB) { + requireCovers(envA, envB); + } + + @Override + public boolean isDetermined() { + return intersectsExteriorOf(RelateGeometry.GEOM_A); + } + + @Override + public boolean valueIM() { + return intMatrix.isContains(); + } + }; + } + + /** + * Creates a predicate to determine whether a geometry is within another geometry. + *

+ * The within predicate has the following equivalent definitions: + *

    + *
  • Every point of this geometry is a point of the other geometry, + * and the interiors of the two geometries have at least one point in common. + *
  • The DE-9IM Intersection Matrix for the two geometries matches + * [T*F**F***] + *
  • contains(B, A) = true + *
    (within is the converse of {@link #contains}) + *
+ * An implication of the definition is that + * "The boundary of a Geometry is not within the Geometry". + * In other words, if a geometry A is a subset of + * the points in the boundary of a geometry B, within(B, A) = false + * (As a concrete example, take A to be a LineString which lies in the boundary of a Polygon B.) + * For a predicate with similar behavior but avoiding + * this subtle limitation, see {@link #coveredBy}. + * + *@return the predicate instance + * + * @see #contains() + */ + public static TopologyPredicate within() { + return new IMPredicate() { + + public String name() { return "within"; } + + @Override + public boolean requiresExteriorCheck(boolean isSourceA) { + //-- only need to check A against Exterior of B + return isSourceA == RelateGeometry.GEOM_A; + } + + @Override + public void init(int dimA, int dimB) { + super.init(dimA, dimB); + require( isDimsCompatibleWithCovers(dimB, dimA) ); + } + + @Override + public void init(Envelope envA, Envelope envB) { + requireCovers(envB, envA); + } + + @Override + public boolean isDetermined() { + return intersectsExteriorOf(RelateGeometry.GEOM_B); + } + + public boolean valueIM() { + return intMatrix.isWithin(); + } + }; + } + + /** + * Creates a predicate to determine whether a geometry covers another geometry. + *

+ * The covers predicate has the following equivalent definitions: + *

    + *
  • Every point of the other geometry is a point of this geometry. + *
  • The DE-9IM Intersection Matrix for the two geometries matches + * at least one of the following patterns: + *
      + *
    • [T*****FF*] + *
    • [*T****FF*] + *
    • [***T**FF*] + *
    • [****T*FF*] + *
    + *
  • coveredBy(b, a) = true + *
    (covers is the converse of {@link #coveredBy}) + *
+ * If either geometry is empty, the value of this predicate is false. + *

+ * This predicate is similar to {@link #contains()}, + * but is more inclusive (i.e. returns true for more cases). + * In particular, unlike contains it does not distinguish between + * points in the boundary and in the interior of geometries. + * For most cases, covers should be used in preference to contains. + * As an added benefit, covers is more amenable to optimization, + * and hence should be more performant. + * + *@return the predicate instance + * + * @see #coveredBy() + */ + public static TopologyPredicate covers() { + return new IMPredicate() { + + public String name() { return "covers"; } + + @Override + public boolean requiresExteriorCheck(boolean isSourceA) { + //-- only need to check B against Exterior of A + return isSourceA == RelateGeometry.GEOM_B; + } + + @Override + public void init(int dimA, int dimB) { + super.init(dimA, dimB); + require( isDimsCompatibleWithCovers(dimA, dimB) ); + } + + @Override + public void init(Envelope envA, Envelope envB) { + requireCovers(envA, envB); + } + + @Override + public boolean isDetermined() { + return intersectsExteriorOf(RelateGeometry.GEOM_A); + } + + @Override + public boolean valueIM() { + return intMatrix.isCovers(); + } + }; + } + + /** + * Creates a predicate to determine whether a geometry is covered by another geometry. + *

+ * The coveredBy predicate has the following equivalent definitions: + *

    + *
  • Every point of this geometry is a point of the other geometry. + *
  • The DE-9IM Intersection Matrix for the two geometries matches + * at least one of the following patterns: + *
      + *
    • [T*F**F***] + *
    • [*TF**F***] + *
    • [**FT*F***] + *
    • [**F*TF***] + *
    + *
  • covers(B, A) = true + *
    (coveredBy is the converse of {@link #covers}) + *
+ * If either geometry is empty, the value of this predicate is false. + *

+ * This predicate is similar to {@link #within}, + * but is more inclusive (i.e. returns true for more cases). + * + *@return the predicate instance + * + * @see #covers() + */ + public static TopologyPredicate coveredBy() { + return new IMPredicate() { + public String name() { return "coveredBy"; } + + @Override + public boolean requiresExteriorCheck(boolean isSourceA) { + //-- only need to check A against Exterior of B + return isSourceA == RelateGeometry.GEOM_A; + } + + @Override + public void init(int dimA, int dimB) { + super.init(dimA, dimB); + require( isDimsCompatibleWithCovers(dimB, dimA) ); + } + + @Override + public void init(Envelope envA, Envelope envB) { + requireCovers(envB, envA); + } + + @Override + public boolean isDetermined() { + return intersectsExteriorOf(RelateGeometry.GEOM_B); + } + + @Override + public boolean valueIM() { + return intMatrix.isCoveredBy(); + } + }; + } + + /** + * Creates a predicate to determine whether a geometry crosses another geometry. + *

+ * The crosses predicate has the following equivalent definitions: + *

    + *
  • The geometries have some but not all interior points in common. + *
  • The DE-9IM Intersection Matrix for the two geometries matches + * one of the following patterns: + *
      + *
    • [T*T******] (for P/L, P/A, and L/A cases) + *
    • [T*****T**] (for L/P, A/P, and A/L cases) + *
    • [0********] (for L/L cases) + *
    + *
+ * For the A/A and P/P cases this predicate returns false. + *

+ * The SFS defined this predicate only for P/L, P/A, L/L, and L/A cases. + * To make the relation symmetric + * JTS extends the definition to apply to L/P, A/P and A/L cases as well. + * + * @return the predicate instance + */ + public static TopologyPredicate crosses() { + return new IMPredicate() { + public String name() { return "crosses"; } + + @Override + public void init(int dimA, int dimB) { + super.init(dimA, dimB); + boolean isBothPointsOrAreas = (dimA == Dimension.P && dimB == Dimension.P) + || (dimA == Dimension.A && dimB == Dimension.A); + require(! isBothPointsOrAreas); + } + + @Override + public boolean isDetermined() { + if (dimA == Dimension.L && dimB == Dimension.L) { + //-- L/L interaction can only be dim = P + if (getDimension(Location.INTERIOR, Location.INTERIOR) > Dimension.P) + return true; + } + else if (dimA < dimB) { + if (isIntersects(Location.INTERIOR, Location.INTERIOR) + && isIntersects(Location.INTERIOR, Location.EXTERIOR)) { + return true; + } + } + else if (dimA > dimB) { + if (isIntersects(Location.INTERIOR, Location.INTERIOR) + && isIntersects(Location.EXTERIOR, Location.INTERIOR)) { + return true; + } + } + return false; + } + + @Override + public boolean valueIM() { + return intMatrix.isCrosses(dimA, dimB); + } + }; + } + + /** + * Creates a predicate to determine whether two geometries are topologically equal. + *

+ * The equals predicate has the following equivalent definitions: + *

    + *
  • The two geometries have at least one point in common, + * and no point of either geometry lies in the exterior of the other geometry. + *
  • The DE-9IM Intersection Matrix for the two geometries matches + * the pattern T*F**FFF* + *
+ * + * @return the predicate instance + */ + public static TopologyPredicate equalsTopo() { + return new IMPredicate() { + public String name() { return "equals"; } + + @Override + public void init(int dimA, int dimB) { + super.init(dimA, dimB); + require(dimA == dimB); + } + + @Override + public void init(Envelope envA, Envelope envB) { + require(envA.equals(envB)); + } + + @Override + public boolean isDetermined() { + boolean isEitherExteriorIntersects = + isIntersects(Location.INTERIOR, Location.EXTERIOR) + || isIntersects(Location.BOUNDARY, Location.EXTERIOR) + || isIntersects(Location.EXTERIOR, Location.INTERIOR) + || isIntersects(Location.EXTERIOR, Location.BOUNDARY); + + return isEitherExteriorIntersects; + } + + @Override + public boolean valueIM() { + return intMatrix.isEquals(dimA, dimB); + } + }; + } + + /** + * Creates a predicate to determine whether a geometry overlaps another geometry. + *

+ * The overlaps predicate has the following equivalent definitions: + *

    + *
  • The geometries have at least one point each not shared by the other + * (or equivalently neither covers the other), + * they have the same dimension, + * and the intersection of the interiors of the two geometries has + * the same dimension as the geometries themselves. + *
  • The DE-9IM Intersection Matrix for the two geometries matches + * [T*T***T**] (for P/P and A/A cases) + * or [1*T***T**] (for L/L cases) + *
+ * If the geometries are of different dimension this predicate returns false. + * This predicate is symmetric. + * + * @return the predicate instance + */ + public static TopologyPredicate overlaps() { + return new IMPredicate() { + public String name() { return "overlaps"; } + + @Override + public void init(int dimA, int dimB) { + super.init(dimA, dimB); + require(dimA == dimB); + } + + @Override + public boolean isDetermined() { + if (dimA == Dimension.A || dimA == Dimension.P) { + if (isIntersects(Location.INTERIOR, Location.INTERIOR) + && isIntersects(Location.INTERIOR, Location.EXTERIOR) + && isIntersects(Location.EXTERIOR, Location.INTERIOR)) + return true; + } + if (dimA == Dimension.L) { + if (isDimension(Location.INTERIOR, Location.INTERIOR, Dimension.L) + && isIntersects(Location.INTERIOR, Location.EXTERIOR) + && isIntersects(Location.EXTERIOR, Location.INTERIOR)) + return true; + } + return false; + } + + @Override + public boolean valueIM() { + return intMatrix.isOverlaps(dimA, dimB); + } + }; + } + + /** + * Creates a predicate to determine whether a geometry touches another geometry. + *

+ * The touches predicate has the following equivalent definitions: + *

    + *
  • The geometries have at least one point in common, + * but their interiors do not intersect. + *
  • The DE-9IM Intersection Matrix for the two geometries matches + * at least one of the following patterns + *
      + *
    • [FT*******] + *
    • [F**T*****] + *
    • [F***T****] + *
    + *
+ * If both geometries have dimension 0, the predicate returns false, + * since points have only interiors. + * This predicate is symmetric. + * + * @return the predicate instance + */ + public static TopologyPredicate touches() { + return new IMPredicate() { + public String name() { return "touches"; } + + @Override + public void init(int dimA, int dimB) { + super.init(dimA, dimB); + //-- Points have only interiors, so cannot touch + boolean isBothPoints = dimA == 0 && dimB == 0; + require(! isBothPoints); + } + + @Override + public boolean isDetermined() { + //-- for touches interiors cannot intersect + boolean isInteriorsIntersects = isIntersects(Location.INTERIOR, Location.INTERIOR); + return isInteriorsIntersects; + } + + @Override + public boolean valueIM() { + return intMatrix.isTouches(dimA, dimB); + } + }; + } + + /** + * Creates a predicate that matches a DE-9IM matrix pattern. + * + * @param imPattern the pattern to match + * @return a predicate that matches the pattern + * + * @see IntersectionMatrixPattern + */ + public static TopologyPredicate matches(String imPattern) { + return new IMPatternMatcher(imPattern); + } +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateSegmentString.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateSegmentString.java new file mode 100644 index 0000000000..f81485136d --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateSegmentString.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2024 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateArrays; +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.noding.BasicSegmentString; + +/** + * Models a linear edge of a {@link RelateGeometry}. + * + * @author mdavis + * + */ +class RelateSegmentString extends BasicSegmentString { + + public static RelateSegmentString createLine(Coordinate[] pts, boolean isA, int elementId, RelateGeometry parent) { + return createSegmentString(pts, isA, Dimension.L, elementId, -1, null, parent); + } + + public static RelateSegmentString createRing(Coordinate[] pts, boolean isA, int elementId, int ringId, + Geometry poly, RelateGeometry parent) { + return createSegmentString(pts, isA, Dimension.A, elementId, ringId, poly, parent); + } + + private static RelateSegmentString createSegmentString(Coordinate[] pts, boolean isA, int dim, int elementId, int ringId, + Geometry poly, RelateGeometry parent) { + pts = removeRepeatedPoints(pts); + return new RelateSegmentString(pts, isA, dim, elementId, ringId, poly, parent); + } + + private static Coordinate[] removeRepeatedPoints(Coordinate[] pts) { + if (CoordinateArrays.hasRepeatedPoints(pts)) { + pts = CoordinateArrays.removeRepeatedPoints(pts); + } + return pts; + } + + private boolean isA; + private int dimension; + private int id; + private int ringId; + private RelateGeometry inputGeom; + private Geometry parentPolygonal = null; + + private RelateSegmentString(Coordinate[] pts, boolean isA, int dimension, int id, int ringId, Geometry poly, RelateGeometry inputGeom) { + super(pts, null); + this.isA = isA; + this.dimension = dimension; + this.id = id; + this.ringId = ringId; + this.parentPolygonal = poly; + this.inputGeom = inputGeom; + } + + public boolean isA() { + return isA; + } + + public RelateGeometry getGeometry() { + return inputGeom; + } + + public Geometry getPolygonal() { + return parentPolygonal; + } + + public NodeSection createNodeSection(int segIndex, Coordinate intPt) { + boolean isNodeAtVertex = + intPt.equals2D(getCoordinate(segIndex)) + || intPt.equals2D(getCoordinate(segIndex + 1)); + Coordinate prev = prevVertex(segIndex, intPt); + Coordinate next = nextVertex(segIndex, intPt); + NodeSection a = new NodeSection(isA, dimension, id, ringId, parentPolygonal, isNodeAtVertex, prev, intPt, next); + return a; + } + + /** + * + * @param ss + * @param segIndex + * @param pt + * @return the previous vertex, or null if none exists + */ + private Coordinate prevVertex(int segIndex, Coordinate pt) { + Coordinate segStart = getCoordinate(segIndex); + if (! segStart.equals2D(pt)) + return segStart; + //-- pt is at segment start, so get previous vertex + if (segIndex > 0) + return getCoordinate(segIndex - 1); + if (isClosed()) + return prevInRing(segIndex); + return null; + } + + /** + * + * @param ss + * @param segIndex + * @param pt + * @return the next vertex, or null if none exists + */ + private Coordinate nextVertex(int segIndex, Coordinate pt) { + Coordinate segEnd = getCoordinate(segIndex + 1); + if (! segEnd.equals2D(pt)) + return segEnd; + //-- pt is at seg end, so get next vertex + if (segIndex < size() - 2) + return getCoordinate(segIndex + 2); + if (isClosed()) + return nextInRing(segIndex + 1); + //-- segstring is not closed, so there is no next segment + return null; + } + + /** + * Tests if a segment intersection point has that segment as its + * canonical containing segment. + * Segments are half-closed, and contain their start point but not the endpoint, + * except for the final segment in a non-closed segment string, which contains + * its endpoint as well. + * This test ensures that vertices are assigned to a unique segment in a segment string. + * In particular, this avoids double-counting intersections which lie exactly + * at segment endpoints. + * + * @param segIndex the segment the point may lie on + * @param pt the point + * @return true if the segment contains the point + */ + public boolean isContainingSegment(int segIndex, Coordinate pt) { + //-- intersection is at segment start vertex - process it + if (pt.equals2D(getCoordinate(segIndex))) + return true; + if (pt.equals2D(getCoordinate(segIndex+1))) { + boolean isFinalSegment = segIndex == size() - 2; + if (isClosed() || ! isFinalSegment) + return false; + //-- for final segment, process intersections with final endpoint + return true; + } + //-- intersection is interior - process it + return true; + } + + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyComputer.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyComputer.java new file mode 100644 index 0000000000..1fd7d81880 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyComputer.java @@ -0,0 +1,506 @@ +/* + * Copyright (c) 2022 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import java.util.HashMap; +import java.util.Map; + +import org.locationtech.jts.algorithm.PolygonNodeTopology; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Location; +import org.locationtech.jts.geom.Position; +import org.locationtech.jts.util.Assert; + +class TopologyComputer { + + private static final String MSG_GEOMETRY_DIMENSION_UNEXPECTED = "Unexpected combination of geometry dimensions"; + + private TopologyPredicate predicate; + private RelateGeometry geomA; + private RelateGeometry geomB; + private Map nodeMap = new HashMap(); + + public TopologyComputer(TopologyPredicate predicate, RelateGeometry geomA, RelateGeometry geomB) { + this.predicate = predicate; + this.geomA = geomA; + this.geomB = geomB; + + initExteriorDims(); + } + + /** + * Determine a priori partial EXTERIOR topology based on dimensions. + */ + private void initExteriorDims() { + int dimRealA = geomA.getDimensionReal(); + int dimRealB = geomB.getDimensionReal(); + + /** + * For P/L case, P exterior intersects L interior + */ + if (dimRealA == Dimension.P && dimRealB == Dimension.L) { + updateDim(Location.EXTERIOR, Location.INTERIOR, Dimension.L); + } + else if (dimRealA == Dimension.L && dimRealB == Dimension.P) { + updateDim(Location.INTERIOR, Location.EXTERIOR, Dimension.L); + } + /** + * For P/A case, the Area Int and Bdy intersect the Point exterior. + */ + else if (dimRealA == Dimension.P && dimRealB == Dimension.A) { + updateDim(Location.EXTERIOR, Location.INTERIOR, Dimension.A); + updateDim(Location.EXTERIOR, Location.BOUNDARY, Dimension.L); + } + else if (dimRealA == Dimension.A && dimRealB == Dimension.P) { + updateDim(Location.INTERIOR, Location.EXTERIOR, Dimension.A); + updateDim(Location.BOUNDARY, Location.EXTERIOR, Dimension.L); + } + else if (dimRealA == Dimension.L && dimRealB == Dimension.A) { + updateDim(Location.EXTERIOR, Location.INTERIOR, Dimension.A); + } + else if (dimRealA == Dimension.A && dimRealB == Dimension.L) { + updateDim(Location.INTERIOR, Location.EXTERIOR, Dimension.A); + } + //-- cases where one geom is EMPTY + else if (dimRealA == Dimension.FALSE || dimRealB == Dimension.FALSE) { + if (dimRealA != Dimension.FALSE) { + initExteriorEmpty(RelateGeometry.GEOM_A); + } + if (dimRealB != Dimension.FALSE) { + initExteriorEmpty(RelateGeometry.GEOM_B); + } + } + } + + private void initExteriorEmpty(boolean geomNonEmpty) { + int dimNonEmpty = getDimension(geomNonEmpty); + switch (dimNonEmpty) { + case Dimension.P: + updateDim(geomNonEmpty, Location.INTERIOR, Location.EXTERIOR, Dimension.P); + break; + case Dimension.L: + if (getGeometry(geomNonEmpty).hasBoundary()) { + updateDim(geomNonEmpty, Location.BOUNDARY, Location.EXTERIOR, Dimension.P); + } + updateDim(geomNonEmpty, Location.INTERIOR, Location.EXTERIOR, Dimension.L); + break; + case Dimension.A: + updateDim(geomNonEmpty, Location.BOUNDARY, Location.EXTERIOR, Dimension.L); + updateDim(geomNonEmpty, Location.INTERIOR, Location.EXTERIOR, Dimension.A); + break; + } + } + + private RelateGeometry getGeometry(boolean isA) { + return isA ? geomA : geomB; + } + + public int getDimension(boolean isA) { + return getGeometry(isA).getDimension(); + } + + public boolean isAreaArea() { + return getDimension(RelateGeometry.GEOM_A) == Dimension.A + && getDimension(RelateGeometry.GEOM_B) == Dimension.A; + } + + /** + * Indicates whether the input geometries require self-noding + * for correct evaluation of specific spatial predicates. + * Self-noding is required for geometries which may self-cross + * - i.e. lines, and overlapping polygons in GeometryCollections. + * Self-noding is not required for polygonal geometries. + * This ensures that the locations of nodes created by + * crossing segments are computed explicitly. + * This ensures that node locations match in situations + * where a self-crossing and mutual crossing occur at the same logical location. + * E.g. a self-crossing line tested against a single segment + * identical to one of the crossed segments. + * + * @return true if self-noding is required + */ + public boolean isSelfNodingRequired() { + //TODO: change to testing for lines or GC with > 1 polygon + if (geomA.isPointsOrPolygons()) return false; + if (geomB.isPointsOrPolygons()) return false; + return predicate.requiresSelfNoding(); + } + + public boolean isExteriorCheckRequired(boolean isA) { + return predicate.requiresExteriorCheck(isA); + } + + private void updateDim(int locA, int locB, int dimension) { + predicate.updateDimension(locA, locB, dimension); + } + + private void updateDim(boolean isAB, int loc1, int loc2, int dimension) { + if (isAB) { + updateDim(loc1, loc2, dimension); + } + else { + // is ordered BA + updateDim(loc2, loc1, dimension); + } + } + + public boolean isResultKnown() { + return predicate.isKnown(); + } + + public boolean getResult() { + return predicate.value(); + } + + /** + * Finalize the evaluation. + */ + public void finish() { + predicate.finish(); + } + + private NodeSections getNodeSections(Coordinate nodePt) { + NodeSections node = nodeMap.get(nodePt); + if (node == null) { + node = new NodeSections(nodePt); + nodeMap.put(nodePt, node); + } + return node; + } + + public void addIntersection(NodeSection a, NodeSection b) { + if (! a.isSameGeometry(b)) { + updateABIntersection(a, b); + } + //TODO: for self-intersections add virtual nodes to geometry components + + //-- add edges to node to allow full topology update later + addNodeSections(a, b); + } + + private void addNodeSections(NodeSection ns0, NodeSection ns1) { + NodeSections sections = getNodeSections(ns0.nodePt()); + sections.addNodeSection(ns0); + sections.addNodeSection(ns1); + } + + private void updateABIntersection(NodeSection a, NodeSection b) { + if (NodeSection.isProper(a, b)) { + updateABIntersectionProper(a, b); + } + else if (NodeSection.isAreaArea(a, b)) { + updateAreaAreaCross(a, b); + } + updateNodeLocation(a.nodePt(), a, b); + } + + private void updateABIntersectionProper(NodeSection a, NodeSection b) { + int dimA = a.dimension(); + int dimB = b.dimension(); + if (dimA == 2 && dimB == 2) { + //- a proper edge intersection is an edge cross. + updateAreaAreaCross(a, b); + } + else if (dimA == 2 && dimB == 1) { + updateAreaLineCross(a, b); + } + else if (dimA == 1 && dimB == 2) { + updateAreaLineCross(b, a); + } + else if (dimA == 1 && dimB == 1) { + //-- nothing to do here - node topology is updated by caller + } + else { + Assert.shouldNeverReachHere(MSG_GEOMETRY_DIMENSION_UNEXPECTED); + } + } + + private void updateAreaLineCross(NodeSection eArea, NodeSection eLine) { + //TODO: does this give any info apart from node? which is checked by caller + /** + * A proper crossing of a line and and area + * provides limited topological information, + * since the area edge intersection point + * may also be a node of a hole, or of another shell, or both. + * Full topology is determined when the node topology is evaluated. + */ + boolean geomLine = eLine.isA(); + Coordinate nodePt = eArea.nodePt(); + int locLine = getGeometry(geomLine).locateNode(nodePt, eLine.getPolygonal()); + int locArea = getGeometry(eArea.isA()).locateNode(nodePt, eArea.getPolygonal()); + updateDim(eArea.isA(), locArea, locLine, Dimension.P); + } + + private void updateAreaAreaCross(NodeSection a, NodeSection b) { + boolean isProper = NodeSection.isProper(a, b); + /** + * A crossing of area edges determines that the interiors intersect. + */ + if (isProper || PolygonNodeTopology.isCrossing(a.nodePt(), + a.getVertex(0), a.getVertex(1), + b.getVertex(0), b.getVertex(1))) { + updateDim(Location.INTERIOR, Location.INTERIOR, Dimension.A); + } + } + /** + * Adds a basic edge intersection point. + * @param pt + * @param b + * @param a + */ + private void updateNodeLocation(Coordinate pt, NodeSection a, NodeSection b) { + int locA = geomA.locateNode(pt, a.getPolygonal()); + int locB = geomB.locateNode(pt, b.getPolygonal()); + updateDim(locA, locB, Dimension.P); + } + + public void addPointOnPointInterior(Coordinate pt) { + updateDim(Location.INTERIOR, Location.INTERIOR, Dimension.P); + } + + public void addPointOnPointExterior(boolean isGeomA, Coordinate pt) { + updateDim(isGeomA, Location.INTERIOR, Location.EXTERIOR, Dimension.P); + } + + public void addPointOnGeometry(boolean isA, int locTarget, int dimTarget, Coordinate pt) { + updateDim(isA, Location.INTERIOR, locTarget, Dimension.P); + switch (dimTarget) { + case Dimension.P: + return; + case Dimension.L: + /** + * Because zero-length lines are handled, + * a point lying in the exterior of the line target + * may imply either P or L for the Exterior interaction + */ + //TODO: determine if effective dimension of linear target is L? + //updateDim(isGeomA, Location.EXTERIOR, locTarget, Dimension.P); + return; + case Dimension.A: + /** + * If a point intersects an area target, then the area interior and boundary + * must extend beyond the point and thus interact with its exterior. + */ + updateDim(isA, Location.EXTERIOR, Location.INTERIOR, Dimension.A); + updateDim(isA, Location.EXTERIOR, Location.BOUNDARY, Dimension.L); + return; + } + throw new IllegalStateException("Unknown target dimension: " + dimTarget); + } + + public void addLineEndOnGeometry(boolean isLineA, int locLineEnd, int locTarget, int dimTarget, Coordinate pt) { + switch (dimTarget) { + case Dimension.P: + addLineEndOnPoint(isLineA, locLineEnd, locTarget, pt); + return; + case Dimension.L: + addLineEndOnLine(isLineA, locLineEnd, locTarget, pt); + return; + case Dimension.A: + addLineEndOnArea(isLineA, locLineEnd, locTarget, pt); + return; + } + throw new IllegalStateException("Unknown target dimension: " + dimTarget); + } + + private void addLineEndOnPoint(boolean isLineA, int locLineEnd, int locPoint, Coordinate pt) { + updateDim(isLineA, locLineEnd, locPoint, Dimension.P); + } + + private void addLineEndOnLine(boolean isLineA, int locLineEnd, int locLine, Coordinate pt) { + updateDim(isLineA, locLineEnd, locLine, Dimension.P); + /** + * When a line end is in the exterior, some length of the line interior + * must also be in the exterior. + * This works for zero-length lines as well. + */ + + if (locLine == Location.EXTERIOR) { + updateDim(isLineA, Location.INTERIOR, Location.EXTERIOR, Dimension.L); + } + } + + private void addLineEndOnArea(boolean isLineA, int locLineEnd, int locArea, Coordinate pt) { + if (locArea == Location.BOUNDARY) { + updateDim(isLineA, locLineEnd, locArea, Dimension.P); + } + else { + //TODO: handle zero-length lines? + updateDim(isLineA, Location.INTERIOR, locArea, Dimension.L); + updateDim(isLineA, locLineEnd, locArea, Dimension.P); + updateDim(isLineA, Location.EXTERIOR, locArea, Dimension.A); + } + } + + /** + * Adds topology for an area vertex interaction with a target geometry element. + * Assumes the target geometry element has highest dimension + * (i.e. if the point lies on two elements of different dimension, + * the location on the higher dimension element is provided. + * This is the semantic provided by {@link RelatePointLocator}. + *

+ * Note that in a GeometryCollection containing overlapping or adjacent polygons, + * the area vertex location may be INTERIOR instead of BOUNDARY. + * + * @param isAreaA the input that is the area + * @param locArea the location on the area + * @param locTarget the location on the target geometry element + * @param dimTarget the dimension of the target geometry element + * @param pt the point of interaction + */ + public void addAreaVertex(boolean isAreaA, int locArea, int locTarget, int dimTarget, Coordinate pt) { + if (locTarget == Location.EXTERIOR) { + updateDim(isAreaA, Location.INTERIOR, Location.EXTERIOR, Dimension.A); + /** + * If area vertex is on Boundary further topology can be deduced + * from the neighbourhood around the boundary vertex. + * This is always the case for polygonal geometries. + * For GCs, the vertex may be either on boundary or in interior + * (i.e. of overlapping or adjacent polygons) + */ + if (locArea == Location.BOUNDARY) { + updateDim(isAreaA, Location.BOUNDARY, Location.EXTERIOR, Dimension.L); + updateDim(isAreaA, Location.EXTERIOR, Location.EXTERIOR, Dimension.A); + } + return; + } + switch (dimTarget) { + case Dimension.P: + addAreaVertexOnPoint(isAreaA, locArea, pt); + return; + case Dimension.L: + addAreaVertexOnLine(isAreaA, locArea, locTarget, pt); + return; + case Dimension.A: + addAreaVertexOnArea(isAreaA, locArea, locTarget, pt); + return; + } + throw new IllegalStateException("Unknown target dimension: " + dimTarget); + } + + /** + * Updates topology for an area vertex (in Interior or on Boundary) + * intersecting a point. + * Note that because the largest dimension of intersecting target is determined, + * the intersecting point is not part of any other target geometry, + * and hence its neighbourhood is in the Exterior of the target. + * + * @param isAreaA whether the area is the A input + * @param locArea the location of the vertex in the area + * @param pt the point at which topology is being updated + */ + private void addAreaVertexOnPoint(boolean isAreaA, int locArea, Coordinate pt) { + //-- Assert: locArea != EXTERIOR + //-- Assert: locTarget == INTERIOR + /** + * The vertex location intersects the Point. + */ + updateDim(isAreaA, locArea, Location.INTERIOR, Dimension.P); + /** + * The area interior intersects the point's exterior neighbourhood. + */ + updateDim(isAreaA, Location.INTERIOR, Location.EXTERIOR, Dimension.A); + /** + * If the area vertex is on the boundary, + * the area boundary and exterior intersect the point's exterior neighbourhood + */ + if (locArea == Location.BOUNDARY) { + updateDim(isAreaA, Location.BOUNDARY, Location.EXTERIOR, Dimension.L); + updateDim(isAreaA, Location.EXTERIOR, Location.EXTERIOR, Dimension.A); + } + } + + private void addAreaVertexOnLine(boolean isAreaA, int locArea, int locTarget, Coordinate pt) { + //-- Assert: locArea != EXTERIOR + /** + * If an area vertex intersects a line, all we know is the + * intersection at that point. + * e.g. the line may or may not be collinear with the area boundary, + * and the line may or may not intersect the area interior. + * Full topology is determined later by node analysis + */ + updateDim(isAreaA, locArea, locTarget, Dimension.P); + if (locArea == Location.INTERIOR) { + /** + * The area interior intersects the line's exterior neighbourhood. + */ + updateDim(isAreaA, Location.INTERIOR, Location.EXTERIOR, Dimension.A); + } + } + + public void addAreaVertexOnArea(boolean isAreaA, int locArea, int locTarget, Coordinate pt) { + if (locTarget == Location.BOUNDARY) { + if (locArea == Location.BOUNDARY) { + //-- B/B topology is fully computed later by node analysis + updateDim(isAreaA, Location.BOUNDARY, Location.BOUNDARY, Dimension.P); + } + else { + // locArea == INTERIOR + updateDim(isAreaA, Location.INTERIOR, Location.INTERIOR, Dimension.A); + updateDim(isAreaA, Location.INTERIOR, Location.BOUNDARY, Dimension.L); + updateDim(isAreaA, Location.INTERIOR, Location.EXTERIOR, Dimension.A); + } + } + else { + //-- locTarget is INTERIOR or EXTERIOR` + updateDim(isAreaA, Location.INTERIOR, locTarget, Dimension.A); + /** + * If area vertex is on Boundary further topology can be deduced + * from the neighbourhood around the boundary vertex. + * This is always the case for polygonal geometries. + * For GCs, the vertex may be either on boundary or in interior + * (i.e. of overlapping or adjacent polygons) + */ + if (locArea == Location.BOUNDARY) { + updateDim(isAreaA, Location.BOUNDARY, locTarget, Dimension.L); + updateDim(isAreaA, Location.EXTERIOR, locTarget, Dimension.A); + } + } + } + + public void evaluateNodes() { + for (NodeSections nodeSections : nodeMap.values()) { + if (nodeSections.hasInteractionAB()) { + evaluateNode(nodeSections); + if (isResultKnown()) + return; + } + } + } + + private void evaluateNode(NodeSections nodeSections) { + Coordinate p = nodeSections.getCoordinate(); + RelateNode node = nodeSections.createNode(); + //-- Node must have edges for geom, but may also be in interior of a overlapping GC + boolean isAreaInteriorA = geomA.isNodeInArea(p, nodeSections.getPolygonal(RelateGeometry.GEOM_A)); + boolean isAreaInteriorB = geomB.isNodeInArea(p, nodeSections.getPolygonal(RelateGeometry.GEOM_B)); + node.finish(isAreaInteriorA, isAreaInteriorB); + evaluateNodeEdges(node); + } + + private void evaluateNodeEdges(RelateNode node) { + //TODO: collect distinct dim settings by using temporary matrix? + for (RelateEdge e : node.getEdges()) { + //-- An optimization to avoid updates for cases with a linear geometry + if (isAreaArea()) { + updateDim(e.location(RelateGeometry.GEOM_A, Position.LEFT), + e.location(RelateGeometry.GEOM_B, Position.LEFT), Dimension.A); + updateDim(e.location(RelateGeometry.GEOM_A, Position.RIGHT), + e.location(RelateGeometry.GEOM_B, Position.RIGHT), Dimension.A); + } + updateDim(e.location(RelateGeometry.GEOM_A, Position.ON), + e.location(RelateGeometry.GEOM_B, Position.ON), Dimension.L); + } + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicate.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicate.java new file mode 100644 index 0000000000..639427ede9 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicate.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2022 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import org.locationtech.jts.geom.Envelope; + +/** + * The API for strategy classes implementing + * spatial predicates based on the DE-9IM topology model. + * Predicate values for specific geometry pairs can be evaluated by {@link RelateNG}. + * + * @author Martin Davis + */ +public interface TopologyPredicate { + + /** + * Gets the name of the predicate. + * + * @return the predicate name + */ + String name(); + + /** + * Reports whether this predicate requires self-noding for + * geometries which contain crossing edges + * (for example, {@link LineString}s, or {@line GeometryCollection}s + * containing lines or polygons which may self-intersect). + * Self-noding ensures that intersections are computed consistently + * in cases which contain self-crossings and mutual crossings. + *

+ * Most predicates require this, but it can + * be avoided for simple intersection detection + * (such as in {@link RelatePredicate#intersects()} + * and {@link RelatePredicate#disjoint()}. + * Avoiding self-noding improves performance for polygonal inputs. + * + * @return true if self-noding is required. + */ + default boolean requiresSelfNoding() { + return true; + } + + /** + * Reports whether this predicate requires checking if the source input intersects + * the Exterior of the target input. + * This allows some performance optimizations if not required. + * + * @param isSourceA + * @return true if the predicate requires checking whether the source intersects the target exterior + */ + default boolean requiresExteriorCheck(boolean isSourceA) { + return true; + } + + /** + * Initializes the predicate for a specific geometric case. + * This may allow the predicate result to become known + * if it can be inferred from the dimensions. + * + * @param dimA the dimension of geometry A + * @param dimB the dimension of geometry B + * + * @see Dimension + */ + default void init(int dimA, int dimB) { + //-- default if dimensions provide no information + } + + /** + * Initializes the predicate for a specific geometric case. + * This may allow the predicate result to become known + * if it can be inferred from the envelopes. + * + * @param envA the envelope of geometry A + * @param envB the envelope of geometry B + */ + default void init(Envelope envA, Envelope envB) { + //-- default if envelopes provide no information + } + + /** + * Updates the entry in the DE-9IM intersection matrix + * for given {@link Location}s in the input geometries. + *

+ * If this method is called with a {@link Dimension} value + * which is less than the current value for the matrix entry, + * the implementing class should avoid changing the entry + * if this would cause information loss. + * + * @param locA the location on the A axis of the matrix + * @param locB the location on the B axis of the matrix + * @param dimension the dimension value for the entry + * + * @see Dimension + * @see Location + */ + void updateDimension(int locA, int locB, int dimension); + + /** + * Indicates that the value of the predicate can be finalized + * based on its current state. + */ + void finish(); + + /** + * Tests if the predicate value is known. + * + * @return true if the result is known + */ + boolean isKnown(); + + /** + * Gets the current value of the predicate result. + * The value is only valid if {@link #isKnown()} is true. + * + * @return the predicate result value + */ + boolean value(); + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateTracer.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateTracer.java new file mode 100644 index 0000000000..416e21a799 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateTracer.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Location; + +/** + * Traces the evaluation of a {@link TopologyPredicate}. + * + * @author mdavis + * + */ +public class TopologyPredicateTracer { + + /** + * Creates a new predicate tracing the evaluation of a given predicate. + * + * @param pred the predicate to trace + * @return the traceable predicate + */ + public static TopologyPredicate trace(TopologyPredicate pred) { + return new PredicateTracer(pred); + } + + private TopologyPredicateTracer() { + + } + + private static class PredicateTracer implements TopologyPredicate + { + private TopologyPredicate pred; + + private PredicateTracer(TopologyPredicate pred) { + this.pred = pred; + } + + public String name() { return pred.name(); } + + @Override + public boolean requiresSelfNoding() { + return pred.requiresSelfNoding(); + } + + @Override + public boolean requiresExteriorCheck(boolean isSourceA) { + return pred.requiresExteriorCheck(isSourceA); + } + + @Override + public void init(int dimA, int dimB) { + pred.init(dimA, dimB); + checkValue("dimensions"); + } + + @Override + public void init(Envelope envA, Envelope envB) { + pred.init(envA, envB); + checkValue("envelopes"); + } + + @Override + public void updateDimension(int locA, int locB, int dimension) { + String desc = "A:" + Location.toLocationSymbol(locA) + + "/B:" + Location.toLocationSymbol(locB) + + " -> " + dimension; + String ind = ""; + boolean isChanged = isDimChanged(locA, locB, dimension); + if (isChanged) { + ind = " <<< "; + } + System.out.println(desc + ind); + pred.updateDimension(locA, locB, dimension); + if (isChanged) { + checkValue("IM entry"); + } + } + + private boolean isDimChanged(int locA, int locB, int dimension) { + if (pred instanceof IMPredicate) { + return ((IMPredicate) pred).isDimChanged(locA, locB, dimension); + } + return false; + } + + private void checkValue(String source) { + if (pred.isKnown()) { + System.out.println(name() + " = " + pred.value() + + " based on " + source); + } + } + + @Override + public void finish() { + pred.finish(); + } + + @Override + public boolean isKnown() { + return pred.isKnown(); + } + + @Override + public boolean value() { + return pred.value(); + } + + public String toString() { + return pred.toString(); + } + } +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/package-info.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/package-info.java new file mode 100644 index 0000000000..63810b2c3c --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/package-info.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2024 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ + +/** + * Provides classes to implement the RelateNG algorithm + * computes topological relationships of {@link Geometry}s. + * Topology is evaluated based on the + * Dimensionally-Extended 9-Intersection Model (DE-9IM). + * The {@link RelateNG} class supports computing the value of boolean topological predicates. + * Standard OGC named predicates are provided by the {@link RelatePredicate} functions. + * Custom relationships can be specified via testing against DE-9IM matrix patterns + * (see {@link IntersectionMatrixPattern} for examples). + * The full DE-9IM {@link IntersectionMatrix} can also be computed. + *

+ * The algorithm has the following capabilities: + *

    + *
  1. Efficient short-circuited evaluation of topological predicates + * (including matching custom DE-9IM patterns) + *
  2. Optimized repeated evaluation of predicates against a single geometry + * via cached spatial indexes (AKA "prepared mode") + *
  3. Robust computation (since only point-local topology is required, + * so that invalid geometry topology cannot cause failures) + *
  4. Support for mixed-type and overlapping {@link GeometryCollection} inputs + * (using union semantics) + *
  5. Support for {@link BoundaryNodeRule} + *
+ * + * RelateNG operates in 2D only; it ignores any Z ordinates. + * + *

Optimized Short-Circuited Evaluation

+ * The RelateNG algorithm uses strategies to optimize the evaluation of + * topological predicates, including matching DE-9IM matrix patterns. + * These include fast tests of dimensions and envelopes, and short-circuited evaluation + * once the predicate value is known + * (either satisfied or failed) based on the value of matrix entries. + * Named predicates used explicit strategy code. + * DE-9IM matrix pattern matching are short-circuited where possible + * based on analysis of the pattern matrix entries. + * Spatial indexes are used to optimize topological computations + * (such as locating points in geometry elements, + * and analyzing the topological relationship between geometry edges). + * + *

Execution Modes

+ * RelateNG provides two execution modes for evaluating predicates: + *
    + *
  • Single-shot mode evaluates a predicate for a single case of two geometries. + * It is provided by the {@link RelateNG} static functions which take two input geometries. + *
  • Prepared mode optimizes repeated evaluation of predicates + * against a fixed geometry. It is used by creating an instance of {@link RelateNG} + * on the required geometry with the prepare functions, + * and then using the evaluate methods. + * It provides much faster performance for repeated operations against a single geometry. + *
+ * + *

Robustness

+ * RelateNG provides robust evaluation of topological relationships, + * up to the precision of double-precision computation. + * It computes topological relationships in the locality of discrete points, + * without constructing a full topology graph of the inputs. + * This means that invalid input geometries or numerical round-off do not cause exceptions + * (although they may return incorrect answers). + * However, it is necessary to node some inputs together (in particular, linear elements) + * in order to provide consistent evaluation of the topological structure. + * + *

GeometryCollection Handling

+ * {@link GeometryCollection}s may contain geometries of different dimensions, nested to any level. + * The element geometries may overlap in any combination. + * The OGC specification did not provide a definition for the topology + * of GeometryCollections, or how they behave under the DE-9IM model. + * RelateNG defines the topology for arbitrary collections of geometries + * using "union semantics". + * This is specified as: + *
    + *
  • GeometryCollections are evaluated as if they were replaced by the topological union + * of their elements. + *
  • The topological location at a point is equal to its location in the geometry of highest + * dimension which contains it. For example, a point located in the interior of a Polygon + * and the boundary of a LineString has location Interior. + *
+ * + *

Zero-length LineString Handling

+ * Zero-length LineStrings are handled as topologically identical to a Point at the same coordinate. + * + *

Package Specification

+ * + */ +package org.locationtech.jts.operation.relateng; diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/AdjacentEdgeLocatorTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/AdjacentEdgeLocatorTest.java new file mode 100644 index 0000000000..ff8505d29a --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/AdjacentEdgeLocatorTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.Location; + +import junit.textui.TestRunner; +import test.jts.GeometryTestCase; + +public class AdjacentEdgeLocatorTest extends GeometryTestCase { + + public static void main(String args[]) { + TestRunner.run(AdjacentEdgeLocatorTest.class); + } + + public AdjacentEdgeLocatorTest(String name) { + super(name); + } + + public void testAdjacent2() { + checkLocation( + "GEOMETRYCOLLECTION (POLYGON ((1 9, 5 9, 5 1, 1 1, 1 9)), POLYGON ((9 9, 9 1, 5 1, 5 9, 9 9)))", + 5, 5, Location.INTERIOR + ); + } + + public void testNonAdjacent() { + checkLocation( + "GEOMETRYCOLLECTION (POLYGON ((1 9, 4 9, 5 1, 1 1, 1 9)), POLYGON ((9 9, 9 1, 5 1, 5 9, 9 9)))", + 5, 5, Location.BOUNDARY + ); + } + + public void testAdjacent6WithFilledHoles() { + checkLocation( + "GEOMETRYCOLLECTION (POLYGON ((1 9, 5 9, 6 6, 1 5, 1 9), (2 6, 4 8, 6 6, 2 6)), POLYGON ((2 6, 4 8, 6 6, 2 6)), POLYGON ((9 9, 9 5, 6 6, 5 9, 9 9)), POLYGON ((9 1, 5 1, 6 6, 9 5, 9 1), (7 2, 6 6, 8 3, 7 2)), POLYGON ((7 2, 6 6, 8 3, 7 2)), POLYGON ((1 1, 1 5, 6 6, 5 1, 1 1)))", + 6, 6, Location.INTERIOR + ); + } + + public void testAdjacent5WithEmptyHole() { + checkLocation( + "GEOMETRYCOLLECTION (POLYGON ((1 9, 5 9, 6 6, 1 5, 1 9), (2 6, 4 8, 6 6, 2 6)), POLYGON ((2 6, 4 8, 6 6, 2 6)), POLYGON ((9 9, 9 5, 6 6, 5 9, 9 9)), POLYGON ((9 1, 5 1, 6 6, 9 5, 9 1), (7 2, 6 6, 8 3, 7 2)), POLYGON ((1 1, 1 5, 6 6, 5 1, 1 1)))", + 6, 6, Location.BOUNDARY + ); + } + + public void testContainedAndAdjacent() { + String wkt = "GEOMETRYCOLLECTION (POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9)), POLYGON ((9 2, 2 2, 2 8, 9 8, 9 2)))"; + checkLocation(wkt, + 9, 5, Location.BOUNDARY + ); + checkLocation(wkt, + 9, 8, Location.BOUNDARY + ); + } + + /** + * Tests a bug caused by incorrect point-on-segment logic. + */ + public void testDisjointCollinear() { + checkLocation( + "GEOMETRYCOLLECTION (MULTIPOLYGON (((1 4, 4 4, 4 1, 1 1, 1 4)), ((5 4, 8 4, 8 1, 5 1, 5 4))))", + 2, 4, Location.BOUNDARY + ); + } + + private void checkLocation(String wkt, int x, int y, int expectedLoc) { + Geometry geom = read(wkt); + AdjacentEdgeLocator ael = new AdjacentEdgeLocator(geom); + int loc = ael.locate(new Coordinate(x, y)); + assertEquals("Locations are not equal: ", expectedLoc, loc); + } +} diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/PolygonNodeConverterTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/PolygonNodeConverterTest.java new file mode 100644 index 0000000000..8166e92526 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/PolygonNodeConverterTest.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2024 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import java.util.ArrayList; +import java.util.List; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Dimension; + +import junit.textui.TestRunner; +import test.jts.GeometryTestCase; + +public class PolygonNodeConverterTest extends GeometryTestCase { + public static void main(String args[]) { + TestRunner.run(PolygonNodeConverterTest.class); + } + + public PolygonNodeConverterTest(String name) { + super(name); + } + + public void testShells() { + checkConversion( + collect( + sectionShell( 1,1, 5,5, 9,9 ), + sectionShell( 8,9, 5,5, 6,9 ), + sectionShell( 4,9, 5,5, 2,9 ) ), + collect( + sectionShell( 1,1, 5,5, 9,9 ), + sectionShell( 8,9, 5,5, 6,9 ), + sectionShell( 4,9, 5,5, 2,9 ) ) + ); + } + + public void testShellAndHole() { + checkConversion( + collect( + sectionShell( 1,1, 5,5, 9,9 ), + sectionHole( 6,0, 5,5, 4,0 ) ), + collect( + sectionShell( 1,1, 5,5, 4,0 ), + sectionShell( 6,0, 5,5, 9,9 ) ) + ); + } + + public void testShellsAndHoles() { + checkConversion( + collect( + sectionShell( 1,1, 5,5, 9,9 ), + sectionHole( 6,0, 5,5, 4,0 ), + + sectionShell( 8,8, 5,5, 1,8 ), + sectionHole( 4,8, 5,5, 6,8 ) + ), + collect( + sectionShell( 1,1, 5,5, 4,0 ), + sectionShell( 6,0, 5,5, 9,9 ), + + sectionShell( 4,8, 5,5, 1,8 ), + sectionShell( 8,8, 5,5, 6,8 ) + ) + ); + } + + public void testShellAnd2Holes() { + checkConversion( + collect( + sectionShell( 1,1, 5,5, 9,9 ), + sectionHole( 7,0, 5,5, 6,0 ), + sectionHole( 4,0, 5,5, 3,0 ) ), + collect( + sectionShell( 1,1, 5,5, 3,0 ), + sectionShell( 4,0, 5,5, 6,0 ), + sectionShell( 7,0, 5,5, 9,9 ) ) + ); + } + + public void testHoles() { + checkConversion( + collect( + sectionHole( 7,0, 5,5, 6,0 ), + sectionHole( 4,0, 5,5, 3,0 ) ), + collect( + sectionShell( 4,0, 5,5, 6,0 ), + sectionShell( 7,0, 5,5, 3,0 ) ) + ); + } + + private void checkConversion(List input, List expected) { + List actual = PolygonNodeConverter.convert(input); + boolean isEqual = checkSectionsEqual(actual, expected); + if (! isEqual) { + System.out.println("Expected:" + formatSections(expected)); + System.out.println("Actual:" + formatSections(actual)); + } + assertTrue(isEqual); + } + + private String formatSections(List sections) { + StringBuilder sb = new StringBuilder(); + for (NodeSection ns : sections) { + sb.append(ns + "\n"); + } + return sb.toString(); + } + + private boolean checkSectionsEqual(List ns1, List ns2) { + if (ns1.size() != ns2.size()) + return false; + sort(ns1); + sort(ns2); + for (int i = 0; i < ns1.size(); i++) { + int comp = ns1.get(i).compareTo(ns2.get(i)); + if (comp != 0) + return false; + } + return true; + } + + private void sort(List ns) { + ns.sort(new NodeSection.EdgeAngleComparator()); + } + + private List collect(NodeSection... sections) { + List sectionList = new ArrayList(); + for (NodeSection s : sections) { + sectionList.add(s); + } + return sectionList; + } + + private NodeSection sectionHole(double v0x, double v0y, double nx, double ny, double v1x, double v1y) { + return section(1, v0x, v0y, nx, ny, v1x, v1y); + } + + private NodeSection section(int ringId, double v0x, double v0y, double nx, double ny, double v1x, double v1y) { + return new NodeSection(true, Dimension.A, 1, ringId, null, false, + new Coordinate(v0x, v0y), new Coordinate(nx, ny), new Coordinate(v1x, v1y)); + } + + private NodeSection sectionShell(double v0x, double v0y, double nx, double ny, double v1x, double v1y) { + return section(0, v0x, v0y, nx, ny, v1x, v1y); + } +} diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGBoundaryNodeRuleTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGBoundaryNodeRuleTest.java new file mode 100644 index 0000000000..e076d4bc93 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGBoundaryNodeRuleTest.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2024 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ + +package org.locationtech.jts.operation.relateng; + +import org.locationtech.jts.algorithm.BoundaryNodeRule; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.IntersectionMatrix; + +import junit.textui.TestRunner; +import test.jts.GeometryTestCase; + + +/** + * Tests {@link RelateNG} with {@link BoundaryNodeRule}s. + * + * @author Martin Davis + * @version 1.7 + */ +public class RelateNGBoundaryNodeRuleTest + extends GeometryTestCase +{ + public static void main(String args[]) { + TestRunner.run(RelateNGBoundaryNodeRuleTest.class); + } + + public RelateNGBoundaryNodeRuleTest(String name) + { + super(name); + } + + public void testMultiLineStringSelfIntTouchAtEndpoint() + { + String a = "MULTILINESTRING ((20 20, 100 100, 100 20, 20 100), (60 60, 60 140))"; + String b = "LINESTRING (60 60, 20 60)"; + + // under EndPoint, A has a boundary node - A.bdy / B.bdy = 0 + runRelate(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE, "FF1F00102" ); + } + + public void testLineStringSelfIntTouchAtEndpoint() + { + String a = "LINESTRING (20 20, 100 100, 100 20, 20 100)"; + String b = "LINESTRING (60 60, 20 60)"; + + // results for both rules are the same + runRelate(a, b, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE, "F01FF0102" ); + runRelate(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE, "F01FF0102" ); + } + + public void testMultiLineStringTouchAtEndpoint() + { + String a = "MULTILINESTRING ((0 0, 10 10), (10 10, 20 20))"; + String b = "LINESTRING (10 10, 20 0)"; + + // under Mod2, A has no boundary - A.int / B.bdy = 0 +// runRelateTest(a, b, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE, "F01FFF102" ); + // under EndPoint, A has a boundary node - A.bdy / B.bdy = 0 + runRelate(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE, "FF1F00102" ); + // under MultiValent, A has a boundary node but B does not - A.bdy / B.bdy = F and A.int +// runRelateTest(a, b, BoundaryNodeRule.MULTIVALENT_ENDPOINT_BOUNDARY_RULE, "0F1FFF1F2" ); + } + + public void testLineRingTouchAtEndpoints() + { + String a = "LINESTRING (20 100, 20 220, 120 100, 20 100)"; + String b = "LINESTRING (20 20, 20 100)"; + + // under Mod2, A has no boundary - A.int / B.bdy = 0 + runRelate(a, b, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE, "F01FFF102" ); + // under EndPoint, A has a boundary node - A.bdy / B.bdy = 0 + runRelate(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE, "FF1F0F102" ); + // under MultiValent, A has a boundary node but B does not - A.bdy / B.bdy = F and A.int + runRelate(a, b, BoundaryNodeRule.MULTIVALENT_ENDPOINT_BOUNDARY_RULE, "FF10FF1F2" ); + } + + public void testLineRingTouchAtEndpointAndInterior() + { + String a = "LINESTRING (20 100, 20 220, 120 100, 20 100)"; + String b = "LINESTRING (20 20, 40 100)"; + + // this is the same result as for the above test + runRelate(a, b, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE, "F01FFF102" ); + // this result is different - the A node is now on the boundary, so A.bdy/B.ext = 0 + runRelate(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE, "F01FF0102" ); + } + + public void testPolygonEmptyRing() + { + String a = "POLYGON EMPTY"; + String b = "LINESTRING (20 100, 20 220, 120 100, 20 100)"; + + // closed line has no boundary under SFS rule + runRelate(a, b, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE, "FFFFFF1F2" ); + + // closed line has boundary under ENDPOINT rule + runRelate(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE, "FFFFFF102" ); + } + + public void testPolygonEmptyMultiLineStringClosed() + { + String a = "POLYGON EMPTY"; + String b = "MULTILINESTRING ((0 0, 0 1), (0 1, 1 1, 1 0, 0 0))"; + + // closed line has no boundary under SFS rule + runRelate(a, b, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE, "FFFFFF1F2" ); + + // closed line has boundary under ENDPOINT rule + runRelate(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE, "FFFFFF102" ); + } + + void runRelate(String wkt1, String wkt2, BoundaryNodeRule bnRule, String expectedIM) + { + Geometry g1 = read(wkt1); + Geometry g2 = read(wkt2); + IntersectionMatrix im = RelateNG.relate(g1, g2, bnRule); + String imStr = im.toString(); + //System.out.println(imStr); + assertTrue("Expected " + expectedIM + ", found " + im, im.matches(expectedIM)); + } + +} diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGGCTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGGCTest.java new file mode 100644 index 0000000000..140708bf91 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGGCTest.java @@ -0,0 +1,212 @@ +/* + * Copyright (c) 2024 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import junit.textui.TestRunner; + +public class RelateNGGCTest extends RelateNGTestCase { + + public static void main(String args[]) { + TestRunner.run(RelateNGGCTest.class); + } + + public RelateNGGCTest(String name) { + super(name); + } + + public void testDimensionWithEmpty() { + String a = "LINESTRING(0 0, 1 1)"; + String b = "GEOMETRYCOLLECTION(POLYGON EMPTY,LINESTRING(0 0, 1 1))"; + checkCoversCoveredBy(a, b, true); + checkEquals(a, b, true); + } + + // see https://github.com/libgeos/geos/issues/1027 + public void testMP_GLP_GEOS1027() { + String a = "MULTIPOLYGON (((0 0, 3 0, 3 3, 0 3, 0 0)))"; + String b = "GEOMETRYCOLLECTION ( LINESTRING (1 2, 1 1), POINT (0 0))"; + checkRelate(a, b, "1020F1FF2"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkCrosses(a, b, false); + checkEquals(a, b, false); + } + + // see https://github.com/libgeos/geos/issues/1022 + public void testGPL_A() { + String a = "GEOMETRYCOLLECTION (POINT (7 1), LINESTRING (6 5, 6 4))"; + String b = "POLYGON ((7 1, 1 3, 3 9, 7 1))"; + checkRelate(a, b, "F01FF0212"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCrosses(a, b, false); + checkTouches(a, b, true); + checkEquals(a, b, false); + } + + // see https://github.com/libgeos/geos/issues/982 + public void testP_GPL() { + String a = "POINT(0 0)"; + String b = "GEOMETRYCOLLECTION(POINT(0 0), LINESTRING(0 0, 1 0))"; + checkRelate(a, b, "F0FFFF102"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCrosses(a, b, false); + checkTouches(a, b, true); + checkEquals(a, b, false); + } + + public void testLineInOverlappingPolygonsTouchingInteriorEdge() { + String a = "LINESTRING (3 7, 7 3)"; + String b = "GEOMETRYCOLLECTION (POLYGON ((1 9, 7 9, 7 3, 1 3, 1 9)), POLYGON ((9 1, 3 1, 3 7, 9 7, 9 1)))"; + checkRelate(a, b, "1FF0FF212"); + checkContainsWithin(b, a, true); + } + + public void testLineInOverlappingPolygonsCrossingInteriorEdgeAtVertex() { + String a = "LINESTRING (2 2, 8 8)"; + String b = "GEOMETRYCOLLECTION (POLYGON ((1 1, 1 7, 7 7, 7 1, 1 1)), POLYGON ((9 9, 9 3, 3 3, 3 9, 9 9)))"; + checkRelate(a, b, "1FF0FF212"); + checkContainsWithin(b, a, true); + } + + public void testLineInOverlappingPolygonsCrossingInteriorEdgeProper() { + String a = "LINESTRING (2 4, 6 8)"; + String b = "GEOMETRYCOLLECTION (POLYGON ((1 1, 1 7, 7 7, 7 1, 1 1)), POLYGON ((9 9, 9 3, 3 3, 3 9, 9 9)))"; + checkRelate(a, b, "1FF0FF212"); + checkContainsWithin(b, a, true); + } + + public void testPolygonInOverlappingPolygonsTouchingBoundaries() { + String a = "GEOMETRYCOLLECTION (POLYGON ((1 9, 6 9, 6 4, 1 4, 1 9)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1)) )"; + String b = "POLYGON ((2 6, 6 2, 8 4, 4 8, 2 6))"; + checkRelate(a, b, "212F01FF2"); + checkContainsWithin(a, b, true); + } + + public void testLineInOverlappingPolygonsBoundaries() { + String a = "LINESTRING (1 6, 9 6, 9 1, 1 1, 1 6)"; + String b = "GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 6, 6 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1)))"; + checkRelate(a, b, "F1FFFF2F2"); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkCoversCoveredBy(b, a, true); + } + + public void testLineCoversOverlappingPolygonsBoundaries() { + String a = "LINESTRING (1 6, 9 6, 9 1, 1 1, 1 6)"; + String b = "GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 6, 6 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1)))"; + checkRelate(a, b, "F1FFFF2F2"); + checkContainsWithin(b, a, false); + checkCoversCoveredBy(b, a, true); + } + + public void testAdjacentPolygonsContainedInAdjacentPolygons() { + String a = "GEOMETRYCOLLECTION (POLYGON ((2 2, 2 5, 4 5, 4 2, 2 2)), POLYGON ((8 2, 4 3, 4 4, 8 5, 8 2)))"; + String b = "GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 4 6, 4 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1)))"; + checkRelate(a, b, "2FF1FF212"); + checkContainsWithin(b, a, true); + checkCoversCoveredBy(b, a, true); + } + + public void testGCMultiPolygonIntersectsPolygon() { + String a = "POLYGON ((2 5, 3 5, 3 3, 2 3, 2 5))"; + String b = "GEOMETRYCOLLECTION (MULTIPOLYGON (((1 4, 4 4, 4 1, 1 1, 1 4)), ((5 4, 8 4, 8 1, 5 1, 5 4))))"; + checkRelate(a, b, "212101212"); + checkIntersectsDisjoint(a, b, true); + checkCoversCoveredBy(b, a, false); + } + + public void testPolygonContainsGCMultiPolygonElement() { + String a = "POLYGON ((0 5, 4 5, 4 1, 0 1, 0 5))"; + String b = "GEOMETRYCOLLECTION (MULTIPOLYGON (((1 4, 3 4, 3 2, 1 2, 1 4)), ((6 4, 8 4, 8 2, 6 2, 6 4))))"; + checkRelate(a, b, "212FF1212"); + checkIntersectsDisjoint(a, b, true); + checkCoversCoveredBy(b, a, false); + } + + /** + * Demonstrates the need for assigning computed nodes to their rings, + * so that subsequent PIP testing can report node as being on ring boundary. + */ + public void testPolygonOverlappingGCPolygon() { + String a = "GEOMETRYCOLLECTION (POLYGON ((18.6 40.8, 16.8825 39.618567, 16.9319 39.5461, 17.10985 39.485133, 16.6143 38.4302, 16.43145 38.313267, 16.2 37.5, 14.8 37.8, 14.96475 40.474933, 18.6 40.8)))"; + String b = "POLYGON ((16.3649953125 38.37219358064516, 16.3649953125 39.545924774193544, 17.949465625000002 39.545924774193544, 17.949465625000002 38.37219358064516, 16.3649953125 38.37219358064516))"; + checkRelate(b, a, "212101212"); + checkRelate(a, b, "212101212"); + checkIntersectsDisjoint(a, b, true); + checkCoversCoveredBy(a, b, false); + } + + static final String wktAdjacentPolys = "GEOMETRYCOLLECTION (POLYGON ((5 5, 2 9, 9 9, 9 5, 5 5)), POLYGON ((3 1, 5 5, 9 5, 9 1, 3 1)), POLYGON ((1 9, 2 9, 5 5, 3 1, 1 1, 1 9)))"; + + public void testAdjPolygonsCoverPolygonWithEndpointInside() { + String a = wktAdjacentPolys; + String b = "POLYGON ((3 7, 7 7, 7 3, 3 3, 3 7))"; + checkRelate(b, a, "2FF1FF212"); + checkRelate(a, b, "212FF1FF2"); + checkIntersectsDisjoint(a, b, true); + checkCoversCoveredBy(a, b, true); + } + + public void testAdjPolygonsCoverPointAtNode() { + String a = wktAdjacentPolys; + String b = "POINT (5 5)"; + checkRelate(b, a, "0FFFFF212"); + checkRelate(a, b, "0F2FF1FF2"); + checkIntersectsDisjoint(a, b, true); + checkCoversCoveredBy(a, b, true); + } + + public void testAdjPolygonsCoverPointOnEdge() { + String a = wktAdjacentPolys; + String b = "POINT (7 5)"; + checkRelate(b, a, "0FFFFF212"); + checkRelate(a, b, "0F2FF1FF2"); + checkIntersectsDisjoint(a, b, true); + checkCoversCoveredBy(a, b, true); + } + + public void testAdjPolygonsContainingPolygonTouchingInteriorEndpoint() { + String a = wktAdjacentPolys; + String b = "POLYGON ((5 5, 7 5, 7 3, 5 3, 5 5))"; + checkRelate(a, b, "212FF1FF2"); + checkIntersectsDisjoint(a, b, true); + checkCoversCoveredBy(a, b, true); + } + + public void testAdjPolygonsOverlappedByPolygonWithHole() { + String a = wktAdjacentPolys; + String b = "POLYGON ((0 10, 10 10, 10 0, 0 0, 0 10), (2 8, 8 8, 8 2, 2 2, 2 8))"; + checkRelate(a, b, "2121FF212"); + checkIntersectsDisjoint(a, b, true); + checkCoversCoveredBy(a, b, false); + } + + public void testAdjPolygonsContainingLine() { + String a = wktAdjacentPolys; + String b = "LINESTRING (5 5, 7 7)"; + checkRelate(a, b, "102FF1FF2"); + checkIntersectsDisjoint(a, b, true); + checkCoversCoveredBy(a, b, true); + } + + public void testAdjPolygonsContainingLineAndPoint() { + String a = wktAdjacentPolys; + String b = "GEOMETRYCOLLECTION (POINT (5 5), LINESTRING (5 7, 7 7))"; + checkRelate(a, b, "102FF1FF2"); + checkIntersectsDisjoint(a, b, true); + checkCoversCoveredBy(a, b, true); + } + + +} diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGRobustnessTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGRobustnessTest.java new file mode 100644 index 0000000000..f9a7dd2357 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGRobustnessTest.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2024 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import junit.textui.TestRunner; + +/** + * Tests from reported cases with robustness issues. + * + * @author mdavis + * + */ +public class RelateNGRobustnessTest extends RelateNGTestCase { + + public static void main(String args[]) { + TestRunner.run(RelateNGRobustnessTest.class); + } + + public RelateNGRobustnessTest(String name) { + super(name); + } + + //-------------------------------------------------------- + // GeometryCollection semantics + //-------------------------------------------------------- + + // see https://github.com/libgeos/geos/issues/1033 + public void testGEOS_1033() { + checkContainsWithin("POLYGON((1 0,0 4,2 2,1 0))", + "GEOMETRYCOLLECTION(POINT(2 2),POINT(1 0),LINESTRING(1 2,1 1))", + true); + } + + // https://github.com/libgeos/geos/issues/1027 + public void testGEOS_1027() { + checkCoversCoveredBy("MULTIPOLYGON (((0 0, 3 0, 3 3, 0 3, 0 0)))", + "GEOMETRYCOLLECTION ( LINESTRING (1 2, 1 1), POINT (0 0))", + true); + } + + // https://github.com/libgeos/geos/issues/1022 + public void testGEOS_1022() { + checkCrosses("GEOMETRYCOLLECTION (POINT (7 1), LINESTRING (6 5, 6 4))", + "POLYGON ((7 1, 1 3, 3 9, 7 1))", + false); + } + + // https://github.com/libgeos/geos/issues/1011 + public void testGEOS_1011() { + String a = "LINESTRING(75 15,55 43)"; + String b = "GEOMETRYCOLLECTION(POLYGON EMPTY,LINESTRING(75 15,55 43))"; + checkCoversCoveredBy(a, b, true); + checkEquals(a, b, true); + } + + // https://github.com/libgeos/geos/issues/983 + public void testGEOS_983() { + String a = "POINT(0 0)"; + String b = "GEOMETRYCOLLECTION(POINT (1 1), LINESTRING (1 1, 2 2))"; + checkIntersectsDisjoint(a, b, false); + } + + // https://github.com/libgeos/geos/issues/982 + public void testGEOS_982() { + String a = "POINT(0 0)"; + String b1 = "GEOMETRYCOLLECTION(POINT(0 0), LINESTRING(0 0, 1 0))"; + checkContainsWithin(b1, a, false); + checkCoversCoveredBy(b1, a, true); + + String b2 = "GEOMETRYCOLLECTION(LINESTRING(0 0, 1 0), POINT(0 0))"; + checkContainsWithin(b2, a, false); + checkCoversCoveredBy(b2, a, true); + } + + // https://github.com/libgeos/geos/issues/981 + public void testGEOS_981() { + String a = "POINT(0 0)"; + String b = "GEOMETRYCOLLECTION(LINESTRING(0 1, 0 0), POINT(0 0))"; + checkRelateMatches(b, a, IntersectionMatrixPattern.CONTAINS_PROPERLY, false); + } + + + //-------------------------------------------------------- + // Noding robustness problems + //-------------------------------------------------------- + + // https://github.com/libgeos/geos/issues/1053 + public void testGEOS_1053() { + String a = "MULTILINESTRING((2 4, 10 10),(15 10,10 5,5 10))"; + String b = "MULTILINESTRING((2 4, 10 10))"; + checkRelate(a, b, "1F1F00FF2"); + } + + // https://github.com/libgeos/geos/issues/968 + public void testGEOS_968() { + String a2 = "LINESTRING(10 0, 0 20)"; + String b2 = "POINT (9 2)"; + checkCoversCoveredBy(a2, b2, true); + } + + public void xtestGEOS_968_2() { + String a = "LINESTRING(1 0, 0 2)"; + String b = "POINT (0.9 0.2)"; + //-- this case doesn't work due to numeric rounding for Orientation test + checkCoversCoveredBy(a, b,true); + } + + // https://github.com/libgeos/geos/issues/933 + public void testGEOS_933() { + String a = "LINESTRING (0 0, 1 1)"; + String b = "LINESTRING (0.2 0.2, 0.5 0.5)"; + checkCoversCoveredBy(a, b, true); + } + + // https://github.com/libgeos/geos/issues/740 + public void testGEOS_740() { + String a = "POLYGON ((1454700 -331500, 1455100 -330700, 1455466.6191038645 -331281.94727476506, 1455467.8182005754 -331293.26796732045, 1454700 -331500))"; + String b = "LINESTRING (1455389.376551584 -331255.3803222172, 1455467.2422460222 -331287.83037053316)"; + checkContainsWithin(a, b, false); + } + + //-------------------------------------------------------- + // Robustness failures (TopologyException in old code) + //-------------------------------------------------------- + + // https://github.com/libgeos/geos/issues/766 + public void testGEOS_766() { + String a = "POLYGON ((26639.240191093646 6039.3615818717535, 26639.240191093646 5889.361620883223, 28000.000095100608 5889.362081553552, 28000.000095100608 6039.361620882992, 28700.00019021402 6039.361620882992, 28700.00019021402 5889.361822800367, 29899.538842431968 5889.362160452064, 32465.59665091549 5889.362882757903, 32969.2837182586 -1313.697771558439, 31715.832811969216 -1489.87008918589, 31681.039836323587 -1242.3030298361555, 32279.3890331618 -1158.210534269224, 32237.63710287376 -861.1301136466199, 32682.89764107368 -802.0828534499739, 32247.445200905553 5439.292852892075, 31797.06861513178 5439.292852892075, 31797.06861513178 5639.36178850523, 29899.538849750803 5639.361268079038, 26167.69458275995 5639.3602445643955, 26379.03654594742 2617.0293071870683, 26778.062167926924 2644.9318977193907, 26792.01346261031 2445.419086759444, 26193.472956813417 2403.5650586598513, 25939.238114175267 6039.361685403233, 26639.240191093646 6039.3615818717535), (32682.89764107368 -802.0828534499738, 32682.89764107378 -802.0828534499669, 32247.445200905655 5439.292852892082, 32247.445200905553 5439.292852892075, 32682.89764107368 -802.0828534499738))"; + String b = "POLYGON ((32450.100392347143 5889.362314133216, 32050.104955691 5891.272957209961, 32100.021071878822 16341.272221116333, 32500.016508656867 16339.361578039587, 32450.100392347143 5889.362314133216))"; + checkIntersectsDisjoint(a, b, true); + } + + // https://github.com/libgeos/geos/issues/1026 + public void testGEOS_1026() { + String a = "POLYGON((335645.7810000004 5677846.65,335648.6579999998 5677845.801999999,335650.8630842535 5677845.143617179,335650.77673334075 5677844.7250704905,335642.90299999993 5677847.498,335645.7810000004 5677846.65))"; + String b = "POLYGON((335642.903 5677847.498,335642.894 5677847.459,335645.92 5677846.69,335647.378 5677852.523,335644.403 5677853.285,335644.374 5677853.293,335642.903 5677847.498))"; + checkTouches(a, b, false); + } + + // https://github.com/libgeos/geos/issues/1069 =- too large to reproduce here + + // https://trac.osgeo.org/postgis/ticket/5583 =- too large to reproduce here + + // https://github.com/locationtech/jts/issues/1051 + public void testJTS_1051() { + String a = "POLYGON ((414188.5999999999 6422867.1, 414193.7 6422866.5, 414205.1 6422859.4, 414223.7 6422846.8, 414229.6 6422843.2, 414235.2 6422835.4, 414224.7 6422837.9, 414219.4 6422842.1, 414210.9 6422849, 414199.2 6422857.6, 414191.1 6422863.4, 414188.5999999999 6422867.1))"; + String b = "LINESTRING (414187.2 6422831.6, 414179 6422836.1, 414182.2 6422841.8, 414176.7 6422844, 414184.5 6422859.5, 414188.6 6422867.1)"; + checkIntersectsDisjoint(a, b, true); + } + + // https://trac.osgeo.org/postgis/ticket/5362 + public void testPostGIS_5362() { + String a = "POLYGON ((-707259.66 -1121493.36, -707205.9 -1121605.808, -707310.5388 -1121540.5446, -707318.8200000001 -1121533.21, -707259.66 -1121493.36))"; + String b = "POLYGON ((-707356.18 -1121550.69, -707332.82 -1121536.63, -707318.82 -1121533.21, -707321.72 -1121535.08, -707327.4 -1121539.21, -707356.18 -1121550.69))"; + checkRelate(a, b, "2F2101212"); + checkIntersectsDisjoint(a, b, true); + } + + //-------------------------------------------------------- + // Topological Inconsistency + //-------------------------------------------------------- + + // https://github.com/libgeos/geos/issues/1064 + public void testGEOS_1064() { + String a = "LINESTRING (16.330791631988802 68.75635661578073, 16.332533372319826 68.75496886016562)"; + String b = "LINESTRING (16.30641253121884 68.75189557630306, 16.33167771310482 68.75565061843871)"; + checkRelate(a, b, "F01FF0102"); + } + + // https://github.com/locationtech/jts/issues/396 + public void testJTS_396() { + String a = "LINESTRING (1 0, 0 2, 0 0, 2 2)"; + String b = "LINESTRING (0 0, 2 2)"; + checkRelate(a, b, "101F00FF2"); + checkCoversCoveredBy(a, b, true); + } + +//https://github.com/locationtech/jts/issues/270 + public void testJTS_270() { + String a = "LINESTRING(0.0 0.0, -10.0 1.2246467991473533E-15)"; + String b = "LINESTRING(-9.999143275740073 -0.13089595571333978, -10.0 1.0535676356486768E-13)"; + checkRelate(a, b, "FF10F0102"); + checkIntersectsDisjoint(a, b, true); + } + +} diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTest.java new file mode 100644 index 0000000000..1ba16d1c63 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTest.java @@ -0,0 +1,589 @@ +/* + * Copyright (c) 2022 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import junit.textui.TestRunner; + +public class RelateNGTest extends RelateNGTestCase { + + public static void main(String args[]) { + TestRunner.run(RelateNGTest.class); + } + + public RelateNGTest(String name) { + super(name); + } + + public void testDisjoint() { + String a = "POINT (0 0)"; + String b = "POINT (1 1)"; + checkIntersectsDisjoint(a, b, false); + checkContainsWithin(a, b, false); + checkEquals(a, b, false); + checkRelate(a, b, "FF0FFF0F2"); + } + + //======= P/P ============= + + public void testPointsContained() { + String a = "MULTIPOINT (0 0, 1 1, 2 2)"; + String b = "MULTIPOINT (1 1, 2 2)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkEquals(a, b, false); + checkRelate(a, b, "0F0FFFFF2"); + } + + public void testPointsEqual() { + String a = "MULTIPOINT (0 0, 1 1, 2 2)"; + String b = "MULTIPOINT (0 0, 1 1, 2 2)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkEquals(a, b, true); + } + + public void testValidateRelatePP_13() { + String a = "MULTIPOINT ((80 70), (140 120), (20 20), (200 170))"; + String b = "MULTIPOINT ((80 70), (140 120), (80 170), (200 80))"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkContainsWithin(b, a, false); + checkCoversCoveredBy(a, b, false); + checkOverlaps(a, b, true); + checkTouches(a, b, false); + } + + //======= L/P ============= + + public void testLinePointContains() { + String a = "LINESTRING (0 0, 1 1, 2 2)"; + String b = "MULTIPOINT (0 0, 1 1, 2 2)"; + checkRelate(a, b, "0F10FFFF2"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkContainsWithin(b, a, false); + checkCoversCoveredBy(a, b, true); + checkCoversCoveredBy(b, a, false); + } + + public void testLinePointOverlaps() { + String a = "LINESTRING (0 0, 1 1)"; + String b = "MULTIPOINT (0 0, 1 1, 2 2)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkContainsWithin(b, a, false); + checkCoversCoveredBy(a, b, false); + checkCoversCoveredBy(b, a, false); + } + + public void testZeroLengthLinePoint() { + String a = "LINESTRING (0 0, 0 0)"; + String b = "POINT (0 0)"; + checkRelate(a, b, "0FFFFFFF2"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkContainsWithin(b, a, true); + checkCoversCoveredBy(a, b, true); + checkCoversCoveredBy(b, a, true); + checkEquals(a, b, true); + } + + public void testZeroLengthLineLine() { + String a = "LINESTRING (10 10, 10 10, 10 10)"; + String b = "LINESTRING (10 10, 10 10)"; + checkRelate(a, b, "0FFFFFFF2"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkContainsWithin(b, a, true); + checkCoversCoveredBy(a, b, true); + checkCoversCoveredBy(b, a, true); + checkEquals(a, b, true); + } + + public void testLinePointIntAndExt() { + String a = "MULTIPOINT((60 60), (100 100))"; + String b = "LINESTRING(40 40, 80 80)"; + checkRelate(a, b, "0F0FFF102"); + } + + //======= L/L ============= + + public void testLinesCrossProper() { + String a = "LINESTRING (0 0, 9 9)"; + String b = "LINESTRING(0 9, 9 0)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + } + + public void testLinesOverlap() { + String a = "LINESTRING (0 0, 5 5)"; + String b = "LINESTRING(3 3, 9 9)"; + checkIntersectsDisjoint(a, b, true); + checkTouches(a, b, false); + checkOverlaps(a, b, true); + } + + public void testLinesCrossVertex() { + String a = "LINESTRING (0 0, 8 8)"; + String b = "LINESTRING(0 8, 4 4, 8 0)"; + checkIntersectsDisjoint(a, b, true); + } + + public void testLinesTouchVertex() { + String a = "LINESTRING (0 0, 8 0)"; + String b = "LINESTRING(0 8, 4 0, 8 8)"; + checkIntersectsDisjoint(a, b, true); + } + + public void testLinesDisjointByEnvelope() { + String a = "LINESTRING (0 0, 9 9)"; + String b = "LINESTRING(10 19, 19 10)"; + checkIntersectsDisjoint(a, b, false); + checkContainsWithin(a, b, false); + } + + public void testLinesDisjoint() { + String a = "LINESTRING (0 0, 9 9)"; + String b = "LINESTRING (4 2, 8 6)"; + checkIntersectsDisjoint(a, b, false); + checkContainsWithin(a, b, false); + } + + public void testLinesClosedEmpty() { + String a = "MULTILINESTRING ((0 0, 0 1), (0 1, 1 1, 1 0, 0 0))"; + String b = "LINESTRING EMPTY"; + checkRelate(a, b, "FF1FFFFF2"); + checkIntersectsDisjoint(a, b, false); + checkContainsWithin(a, b, false); + } + + public void testLinesRingTouchAtNode() { + String a = "LINESTRING (5 5, 1 8, 1 1, 5 5)"; + String b = "LINESTRING (5 5, 9 5)"; + checkRelate(a, b, "F01FFF102"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkTouches(a, b, true); + } + + public void testLinesTouchAtBdy() { + String a = "LINESTRING (5 5, 1 8)"; + String b = "LINESTRING (5 5, 9 5)"; + checkRelate(a, b, "FF1F00102"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkTouches(a, b, true); + } + + public void testLinesOverlapWithDisjointLine() { + String a = "LINESTRING (1 1, 9 9)"; + String b = "MULTILINESTRING ((2 2, 8 8), (6 2, 8 4))"; + checkRelate(a, b, "101FF0102"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkOverlaps(a, b, true); + } + + public void testLinesDisjointOverlappingEnvelopes() { + String a = "LINESTRING (60 0, 20 80, 100 80, 80 120, 40 140)"; + String b = "LINESTRING (60 40, 140 40, 140 160, 0 160)"; + checkRelate(a, b, "FF1FF0102"); + checkIntersectsDisjoint(a, b, false); + checkContainsWithin(a, b, false); + checkTouches(a, b, false); + } + + /** + * Case from https://github.com/locationtech/jts/issues/270 + * Strictly, the lines cross, since their interiors intersect + * according to the Orientation predicate. + * However, the computation of the intersection point is + * non-robust, and reports it as being equal to the endpoint + * POINT (-10 0.0000000000000012) + * For consistency the relate algorithm uses the intersection node topology. + */ + public void testLinesCross_JTS270() { + String a = "LINESTRING (0 0, -10 0.0000000000000012)"; + String b = "LINESTRING (-9.999143275740073 -0.1308959557133398, -10 0.0000000000001054)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkCrosses(a, b, false); + checkOverlaps(a, b, false); + checkTouches(a, b, true); + } + + public void testLinesContained_JTS396() { + String a = "LINESTRING (1 0, 0 2, 0 0, 2 2)"; + String b = "LINESTRING (0 0, 2 2)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkCoversCoveredBy(a, b, true); + checkCrosses(a, b, false); + checkOverlaps(a, b, false); + checkTouches(a, b, false); + } + + + /** + * This case shows that lines must be self-noded, + * so that node topology is constructed correctly + * (at least for some predicates). + */ + public void testLinesContainedWithSelfIntersection() { + String a = "LINESTRING (2 0, 0 2, 0 0, 2 2)"; + String b = "LINESTRING (0 0, 2 2)"; + //checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkCoversCoveredBy(a, b, true); + checkCrosses(a, b, false); + checkOverlaps(a, b, false); + checkTouches(a, b, false); + } + + public void testLineContainedInRing() { + String a = "LINESTRING(60 60, 100 100, 140 60)"; + String b = "LINESTRING(100 100, 180 20, 20 20, 100 100)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(b, a, true); + checkCoversCoveredBy(b, a, true); + checkCrosses(a, b, false); + checkOverlaps(a, b, false); + checkTouches(a, b, false); + } + + // see https://github.com/libgeos/geos/issues/933 + public void testLineLineProperIntersection() { + String a = "MULTILINESTRING ((0 0, 1 1), (0.5 0.5, 1 0.1, -1 0.1))"; + String b = "LINESTRING (0 0, 1 1)"; + //checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkCoversCoveredBy(a, b, true); + checkCrosses(a, b, false); + checkOverlaps(a, b, false); + checkTouches(a, b, false); + } + + public void testLineSelfIntersectionCollinear() { + String a = "LINESTRING (9 6, 1 6, 1 0, 5 6, 9 6)"; + String b = "LINESTRING (9 9, 3 1)"; + checkRelate(a, b, "0F1FFF102"); + } + + //======= A/P ============= + + public void testPolygonPointInside() { + String a = "POLYGON ((0 10, 10 10, 10 0, 0 0, 0 10))"; + String b = "POINT (1 1)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + } + + public void testPolygonPointOutside() { + String a = "POLYGON ((10 0, 0 0, 0 10, 10 0))"; + String b = "POINT (8 8)"; + checkIntersectsDisjoint(a, b, false); + checkContainsWithin(a, b, false); + } + + public void testPolygonPointInBoundary() { + String a = "POLYGON ((10 0, 0 0, 0 10, 10 0))"; + String b = "POINT (1 0)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, true); + } + + public void testAreaPointInExterior() { + String a = "POLYGON ((1 5, 5 5, 5 1, 1 1, 1 5))"; + String b = "POINT (7 7)"; + checkRelate(a, b, "FF2FF10F2"); + checkIntersectsDisjoint(a, b, false); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkTouches(a, b, false); + checkOverlaps(a, b, false); + } + + //======= A/L ============= + + + public void testAreaLineContainedAtLineVertex() { + String a = "POLYGON ((1 5, 5 5, 5 1, 1 1, 1 5))"; + String b = "LINESTRING (2 3, 3 5, 4 3)"; + checkIntersectsDisjoint(a, b, true); + //checkContainsWithin(a, b, true); + //checkCoversCoveredBy(a, b, true); + checkTouches(a, b, false); + checkOverlaps(a, b, false); + } + + public void testAreaLineTouchAtLineVertex() { + String a = "POLYGON ((1 5, 5 5, 5 1, 1 1, 1 5))"; + String b = "LINESTRING (1 8, 3 5, 5 8)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkTouches(a, b, true); + checkOverlaps(a, b, false); + } + + public void testPolygonLineInside() { + String a = "POLYGON ((0 10, 10 10, 10 0, 0 0, 0 10))"; + String b = "LINESTRING (1 8, 3 5, 5 8)"; + checkRelate(a, b, "102FF1FF2"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + } + + public void testPolygonLineOutside() { + String a = "POLYGON ((10 0, 0 0, 0 10, 10 0))"; + String b = "LINESTRING (4 8, 9 3)"; + checkIntersectsDisjoint(a, b, false); + checkContainsWithin(a, b, false); + } + + public void testPolygonLineInBoundary() { + String a = "POLYGON ((10 0, 0 0, 0 10, 10 0))"; + String b = "LINESTRING (1 0, 9 0)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, true); + checkTouches(a, b, true); + checkOverlaps(a, b, false); + } + + public void testPolygonLineCrossingContained() { + String a = "MULTIPOLYGON (((20 80, 180 80, 100 0, 20 80)), ((20 160, 180 160, 100 80, 20 160)))"; + String b = "LINESTRING (100 140, 100 40)"; + checkRelate(a, b, "1020F1FF2"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkCoversCoveredBy(a, b, true); + checkTouches(a, b, false); + checkOverlaps(a, b, false); + } + + public void testValidateRelateLA_220() { + String a = "LINESTRING (90 210, 210 90)"; + String b = "POLYGON ((150 150, 410 150, 280 20, 20 20, 150 150))"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkTouches(a, b, false); + checkOverlaps(a, b, false); + } + + /** + * See RelateLA.xml (line 585) + */ + public void testLineCrossingPolygonAtShellHolePoint() { + String a = "LINESTRING (60 160, 150 70)"; + String b = "POLYGON ((190 190, 360 20, 20 20, 190 190), (110 110, 250 100, 140 30, 110 110))"; + checkRelate(a, b, "F01FF0212"); + checkTouches(a, b, true); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkTouches(a, b, true); + checkOverlaps(a, b, false); + } + + public void testLineCrossingPolygonAtNonVertex() { + String a = "LINESTRING (20 60, 150 60)"; + String b = "POLYGON ((150 150, 410 150, 280 20, 20 20, 150 150))"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkTouches(a, b, false); + checkOverlaps(a, b, false); + } + + public void testPolygonLinesContainedCollinearEdge() { + String a = "POLYGON ((110 110, 200 20, 20 20, 110 110))"; + String b = "MULTILINESTRING ((110 110, 60 40, 70 20, 150 20, 170 40), (180 30, 40 30, 110 80))"; + checkRelate(a, b, "102101FF2"); + } + + //======= A/A ============= + + + public void testPolygonsEdgeAdjacent() { + String a = "POLYGON ((1 3, 3 3, 3 1, 1 1, 1 3))"; + String b = "POLYGON ((5 3, 5 1, 3 1, 3 3, 5 3))"; + //checkIntersectsDisjoint(a, b, true); + checkOverlaps(a, b, false); + checkTouches(a, b, true); + checkOverlaps(a, b, false); + } + + public void testPolygonsEdgeAdjacent2() { + String a = "POLYGON ((1 3, 4 3, 3 0, 1 1, 1 3))"; + String b = "POLYGON ((5 3, 5 1, 3 0, 4 3, 5 3))"; + //checkIntersectsDisjoint(a, b, true); + checkOverlaps(a, b, false); + checkTouches(a, b, true); + checkOverlaps(a, b, false); + } + + public void testPolygonsNested() { + String a = "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))"; + String b = "POLYGON ((2 8, 8 8, 8 2, 2 2, 2 8))"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkCoversCoveredBy(a, b, true); + checkOverlaps(a, b, false); + checkTouches(a, b, false); + } + + public void testPolygonsOverlapProper() { + String a = "POLYGON ((1 1, 1 7, 7 7, 7 1, 1 1))"; + String b = "POLYGON ((2 8, 8 8, 8 2, 2 2, 2 8))"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkOverlaps(a, b, true); + checkTouches(a, b, false); + } + + public void testPolygonsOverlapAtNodes() { + String a = "POLYGON ((1 5, 5 5, 5 1, 1 1, 1 5))"; + String b = "POLYGON ((7 3, 5 1, 3 3, 5 5, 7 3))"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkOverlaps(a, b, true); + checkTouches(a, b, false); + } + + public void testPolygonsContainedAtNodes() { + String a = "POLYGON ((1 5, 5 5, 6 2, 1 1, 1 5))"; + String b = "POLYGON ((1 1, 5 5, 6 2, 1 1))"; + //checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkCoversCoveredBy(a, b, true); + checkOverlaps(a, b, false); + checkTouches(a, b, false); + } + + public void testPolygonsNestedWithHole() { + String a = "POLYGON ((40 60, 420 60, 420 320, 40 320, 40 60), (200 140, 160 220, 260 200, 200 140))"; + String b = "POLYGON ((80 100, 360 100, 360 280, 80 280, 80 100))"; + //checkIntersectsDisjoint(true, a, b); + checkContainsWithin(a, b, false); + checkContainsWithin(b, a, false); + //checkCoversCoveredBy(false, a, b); + //checkOverlaps(true, a, b); + checkPredicate(RelatePredicate.contains(), a, b, false); + //checkTouches(false, a, b); + } + + public void testPolygonsOverlappingWithBoundaryInside() { + String a = "POLYGON ((100 60, 140 100, 100 140, 60 100, 100 60))"; + String b = "MULTIPOLYGON (((80 40, 120 40, 120 80, 80 80, 80 40)), ((120 80, 160 80, 160 120, 120 120, 120 80)), ((80 120, 120 120, 120 160, 80 160, 80 120)), ((40 80, 80 80, 80 120, 40 120, 40 80)))"; + checkRelate(a, b, "21210F212"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkContainsWithin(b, a, false); + checkCoversCoveredBy(a, b, false); + checkOverlaps(a, b, true); + checkTouches(a, b, false); + } + + public void testPolygonsOverlapVeryNarrow() { + String a = "POLYGON ((120 100, 120 200, 200 200, 200 100, 120 100))"; + String b = "POLYGON ((100 100, 100000 110, 100000 100, 100 100))"; + checkRelate(a, b, "212111212"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkContainsWithin(b, a, false); + //checkCoversCoveredBy(false, a, b); + //checkOverlaps(true, a, b); + //checkTouches(false, a, b); + } + + public void testValidateRelateAA_86() { + String a = "POLYGON ((170 120, 300 120, 250 70, 120 70, 170 120))"; + String b = "POLYGON ((150 150, 410 150, 280 20, 20 20, 150 150), (170 120, 330 120, 260 50, 100 50, 170 120))"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkOverlaps(a, b, false); + checkPredicate(RelatePredicate.within(), a, b, false); + checkTouches(a, b, true); + } + + public void testValidateRelateAA_97() { + String a = "POLYGON ((330 150, 200 110, 150 150, 280 190, 330 150))"; + String b = "MULTIPOLYGON (((140 110, 260 110, 170 20, 50 20, 140 110)), ((300 270, 420 270, 340 190, 220 190, 300 270)))"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkOverlaps(a, b, false); + checkPredicate(RelatePredicate.within(), a, b, false); + checkTouches(a, b, true); + } + + public void testAdjacentPolygons() { + String a = "POLYGON ((1 9, 6 9, 6 1, 1 1, 1 9))"; + String b = "POLYGON ((9 9, 9 4, 6 4, 6 9, 9 9))"; + checkRelateMatches(a, b, IntersectionMatrixPattern.ADJACENT, true); + } + + public void testAdjacentPolygonsTouchingAtPoint() { + String a = "POLYGON ((1 9, 6 9, 6 1, 1 1, 1 9))"; + String b = "POLYGON ((9 9, 9 4, 6 4, 7 9, 9 9))"; + checkRelateMatches(a, b, IntersectionMatrixPattern.ADJACENT, false); + } + + public void testAdjacentPolygonsOverlappping() { + String a = "POLYGON ((1 9, 6 9, 6 1, 1 1, 1 9))"; + String b = "POLYGON ((9 9, 9 4, 6 4, 5 9, 9 9))"; + checkRelateMatches(a, b, IntersectionMatrixPattern.ADJACENT, false); + } + + public void testContainsProperlyPolygonContained() { + String a = "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))"; + String b = "POLYGON ((2 8, 5 8, 5 5, 2 5, 2 8))"; + checkRelateMatches(a, b, IntersectionMatrixPattern.CONTAINS_PROPERLY, true); + } + + public void testContainsProperlyPolygonTouching() { + String a = "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))"; + String b = "POLYGON ((9 1, 5 1, 5 5, 9 5, 9 1))"; + checkRelateMatches(a, b, IntersectionMatrixPattern.CONTAINS_PROPERLY, false); + } + + public void testContainsProperlyPolygonsOverlapping() { + String a = "GEOMETRYCOLLECTION (POLYGON ((1 9, 6 9, 6 4, 1 4, 1 9)), POLYGON ((2 4, 6 7, 9 1, 2 4)))"; + String b = "POLYGON ((5 5, 6 5, 6 4, 5 4, 5 5))"; + checkRelateMatches(a, b, IntersectionMatrixPattern.CONTAINS_PROPERLY, true); + } + + //================ Repeated Points ============== + + public void testRepeatedPointLL() { + String a = "LINESTRING(0 0, 5 5, 5 5, 5 5, 9 9)"; + String b = "LINESTRING(0 9, 5 5, 5 5, 5 5, 9 0)"; + checkRelate(a, b, "0F1FF0102"); + checkIntersectsDisjoint(a, b, true); + } + + public void testRepeatedPointAA() { + String a = "POLYGON ((1 9, 9 7, 9 1, 1 3, 1 9))"; + String b = "POLYGON ((1 3, 1 3, 1 3, 3 7, 9 7, 9 7, 1 3))"; + checkRelate(a, b, "212F01FF2"); + } + + + +} diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTestCase.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTestCase.java new file mode 100644 index 0000000000..54088ecd49 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTestCase.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2022 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import org.locationtech.jts.geom.Geometry; + +import test.jts.GeometryTestCase; + +public abstract class RelateNGTestCase extends GeometryTestCase { + + private boolean isTrace = false; + + public RelateNGTestCase(String name) { + super(name); + } + + protected void checkIntersectsDisjoint(String wkta, String wktb, boolean expectedValue) { + checkPredicate(RelatePredicate.intersects(), wkta, wktb, expectedValue); + checkPredicate(RelatePredicate.intersects(), wktb, wkta, expectedValue); + checkPredicate(RelatePredicate.disjoint(), wkta, wktb, ! expectedValue); + checkPredicate(RelatePredicate.disjoint(), wktb, wkta, ! expectedValue); + } + + protected void checkContainsWithin(String wkta, String wktb, boolean expectedValue) { + checkPredicate(RelatePredicate.contains(), wkta, wktb, expectedValue); + checkPredicate(RelatePredicate.within(), wktb, wkta, expectedValue); + } + + protected void checkCoversCoveredBy(String wkta, String wktb, boolean expectedValue) { + checkPredicate(RelatePredicate.covers(), wkta, wktb, expectedValue); + checkPredicate(RelatePredicate.coveredBy(), wktb, wkta, expectedValue); + } + + protected void checkCrosses(String wkta, String wktb, boolean expectedValue) { + checkPredicate(RelatePredicate.crosses(), wkta, wktb, expectedValue); + checkPredicate(RelatePredicate.crosses(), wktb, wkta, expectedValue); + } + + protected void checkOverlaps(String wkta, String wktb, boolean expectedValue) { + checkPredicate(RelatePredicate.overlaps(), wkta, wktb, expectedValue); + checkPredicate(RelatePredicate.overlaps(), wktb, wkta, expectedValue); + } + + protected void checkTouches(String wkta, String wktb, boolean expectedValue) { + checkPredicate(RelatePredicate.touches(), wkta, wktb, expectedValue); + checkPredicate(RelatePredicate.touches(), wktb, wkta, expectedValue); + } + + protected void checkEquals(String wkta, String wktb, boolean expectedValue) { + checkPredicate(RelatePredicate.equalsTopo(), wkta, wktb, expectedValue); + checkPredicate(RelatePredicate.equalsTopo(), wktb, wkta, expectedValue); + } + + protected void checkRelate(String wkta, String wktb, String expectedValue) { + Geometry a = read(wkta); + Geometry b = read(wktb); + RelateMatrixPredicate pred = new RelateMatrixPredicate(); + TopologyPredicate predTrace = trace(pred); + RelateNG.relate(a, b, predTrace); + String actualVal = pred.getIM().toString(); + assertEquals(expectedValue, actualVal); + } + + protected void checkRelateMatches(String wkta, String wktb, String pattern, boolean expectedValue) { + TopologyPredicate pred = RelatePredicate.matches(pattern); + checkPredicate(pred, wkta, wktb, expectedValue); + } + + protected void checkPredicate(TopologyPredicate pred, String wkta, String wktb, boolean expectedValue) { + Geometry a = read(wkta); + Geometry b = read(wktb); + TopologyPredicate predTrace = trace(pred); + boolean actualVal = RelateNG.relate(a, b, predTrace); + assertEquals(expectedValue, actualVal); + } + + TopologyPredicate trace(TopologyPredicate pred) { + if (! isTrace) + return pred; + + System.out.println("----------- Pred: " + pred.name()); + + return TopologyPredicateTracer.trace(pred); + } +} diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelatePointLocatorTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelatePointLocatorTest.java new file mode 100644 index 0000000000..e1ae6867e2 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelatePointLocatorTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.Location; + +import junit.textui.TestRunner; +import test.jts.GeometryTestCase; + +public class RelatePointLocatorTest extends GeometryTestCase { + + public static void main(String args[]) { + TestRunner.run(RelatePointLocatorTest.class); + } + + public RelatePointLocatorTest(String name) { + super(name); + } + + String gcPLA = "GEOMETRYCOLLECTION (POINT (1 1), POINT (2 1), LINESTRING (3 1, 3 9), LINESTRING (4 1, 5 4, 7 1, 4 1), LINESTRING (12 12, 14 14), POLYGON ((6 5, 6 9, 9 9, 9 5, 6 5)), POLYGON ((10 10, 10 16, 16 16, 16 10, 10 10)), POLYGON ((11 11, 11 17, 17 17, 17 11, 11 11)), POLYGON ((12 12, 12 16, 16 16, 16 12, 12 12)))"; + + public void testPoint() { + //String wkt = "GEOMETRYCOLLECTION (POINT(0 0), POINT(1 1))"; + checkLocation(gcPLA, 1, 1, DimensionLocation.POINT_INTERIOR); + checkLocation(gcPLA, 0, 1, Location.EXTERIOR); + } + + public void testPointInLine() { + checkLocation(gcPLA, 3, 8, DimensionLocation.LINE_INTERIOR); + } + + public void testPointInArea() { + checkLocation(gcPLA, 8, 8, DimensionLocation.AREA_INTERIOR); + } + + public void testLine() { + checkLocation(gcPLA, 3, 3, DimensionLocation.LINE_INTERIOR); + checkLocation(gcPLA, 3, 1, DimensionLocation.LINE_BOUNDARY); + } + + public void testLineInArea() { + checkLocation(gcPLA, 11, 11, DimensionLocation.AREA_INTERIOR); + checkLocation(gcPLA, 14, 14, DimensionLocation.AREA_INTERIOR); + } + + public void testArea() { + checkLocation(gcPLA, 8, 8, DimensionLocation.AREA_INTERIOR); + checkLocation(gcPLA, 9, 9, DimensionLocation.AREA_BOUNDARY); + } + + public void testAreaInArea() { + checkLocation(gcPLA, 11, 11, DimensionLocation.AREA_INTERIOR); + checkLocation(gcPLA, 12, 12, DimensionLocation.AREA_INTERIOR); + checkLocation(gcPLA, 10, 10, DimensionLocation.AREA_BOUNDARY); + checkLocation(gcPLA, 16, 16, DimensionLocation.AREA_INTERIOR); + } + + public void testLineNode() { + //checkNodeLocation(gcPLA, 12.1, 12.2, Location.INTERIOR); + checkNodeLocation(gcPLA, 3, 1, Location.BOUNDARY); + } + + private void checkLocation(String wkt, double i, double j, int expected) { + Geometry geom = read(wkt); + RelatePointLocator locator = new RelatePointLocator(geom); + int actual = locator.locateWithDim(new Coordinate(i, j)); + assertEquals(expected, actual); + } + + private void checkNodeLocation(String wkt, double i, double j, int expected) { + Geometry geom = read(wkt); + RelatePointLocator locator = new RelatePointLocator(geom); + int actual = locator.locateNode(new Coordinate(i, j), null); + assertEquals(expected, actual); + } +} diff --git a/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGLinesOverlappingPerfTest.java b/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGLinesOverlappingPerfTest.java new file mode 100644 index 0000000000..5e041af852 --- /dev/null +++ b/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGLinesOverlappingPerfTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2022 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package test.jts.perf.operation.relateng; + +import static org.junit.Assert.assertEquals; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.prep.PreparedGeometry; +import org.locationtech.jts.geom.prep.PreparedGeometryFactory; +import org.locationtech.jts.geom.util.SineStarFactory; +import org.locationtech.jts.operation.relateng.RelateNG; +import org.locationtech.jts.operation.relateng.RelatePredicate; + +import test.jts.perf.PerformanceTestCase; +import test.jts.perf.PerformanceTestRunner; + +public class RelateNGLinesOverlappingPerfTest +extends PerformanceTestCase +{ + + public static void main(String args[]) { + PerformanceTestRunner.run(RelateNGLinesOverlappingPerfTest.class); + } + + private static final int N_ITER = 1; + + static double ORG_X = 100; + static double ORG_Y = ORG_X; + static double SIZE = 2 * ORG_X; + static int N_ARMS = 6; + static double ARM_RATIO = 0.3; + + static int GRID_SIZE = 100; + static double GRID_CELL_SIZE = SIZE / GRID_SIZE; + + static int NUM_CASES = GRID_SIZE * GRID_SIZE; + + private static final int B_SIZE_FACTOR = 20; + private static final GeometryFactory factory = new GeometryFactory(); + + private Geometry geomA; + + private Geometry[] geomB; + + public RelateNGLinesOverlappingPerfTest(String name) { + super(name); + setRunSize(new int[] { 100, 1000, 10000, 100000, + 200000 }); + //setRunSize(new int[] { 200000 }); + setRunIterations(N_ITER); + } + + public void setUp() + { + System.out.println("RelateNG Overlapping Lines perf test"); + System.out.println("SineStar: origin: (" + + ORG_X + ", " + ORG_Y + ") size: " + SIZE + + " # arms: " + N_ARMS + " arm ratio: " + ARM_RATIO); + System.out.println("# Iterations: " + N_ITER); + System.out.println("# B geoms: " + NUM_CASES); + } + + public void startRun(int npts) + { + Geometry sineStar = SineStarFactory.create(new Coordinate(ORG_X, ORG_Y), SIZE, npts, N_ARMS, ARM_RATIO); + geomA = sineStar.getBoundary(); + + int nptsB = npts * B_SIZE_FACTOR / NUM_CASES; + if (nptsB < 10 ) nptsB = 10; + + geomB = createSineStarGrid(NUM_CASES, nptsB); + //geomB = createCircleGrid(NUM_CASES, nptsB); + + System.out.println("\n------- Running with A: line # pts = " + npts + + " B # pts = " + nptsB + " x " + NUM_CASES + " lines"); + + /* + if (npts == 999) { + System.out.println(geomA); + + for (Geometry g : geomB) { + System.out.println(g); + } + } +*/ + } + + public void runIntersectsOld() + { + for (Geometry b : geomB) { + geomA.intersects(b); + } + } + + public void runIntersectsOldPrep() + { + PreparedGeometry pgA = PreparedGeometryFactory.prepare(geomA); + for (Geometry b : geomB) { + pgA.intersects(b); + } + } + + public void runIntersectsNG() + { + for (Geometry b : geomB) { + RelateNG.relate(geomA, b, RelatePredicate.intersects()); + } + } + + public void runIntersectsNGPrep() + { + RelateNG rng = RelateNG.prepare(geomA); + for (Geometry b : geomB) { + rng.evaluate(b, RelatePredicate.intersects()); + } + } + + private Geometry[] createSineStarGrid(int nGeoms, int npts) { + Geometry[] geoms = new Geometry[ NUM_CASES ]; + int index = 0; + for (int i = 0; i < GRID_SIZE; i++) { + for (int j = 0; j < GRID_SIZE; j++) { + double x = GRID_CELL_SIZE/2 + i * GRID_CELL_SIZE; + double y = GRID_CELL_SIZE/2 + j * GRID_CELL_SIZE; + Geometry geom = SineStarFactory.create(new Coordinate(x, y), GRID_CELL_SIZE, npts, N_ARMS, ARM_RATIO); + geoms[index++] = geom.getBoundary(); + } + } + return geoms; + } + + private Geometry[] createCircleGrid(int nGeoms, int npts) { + Geometry[] geoms = new Geometry[ NUM_CASES ]; + int index = 0; + for (int i = 0; i < GRID_SIZE; i++) { + for (int j = 0; j < GRID_SIZE; j++) { + double x = GRID_CELL_SIZE/2 + i * GRID_CELL_SIZE; + double y = GRID_CELL_SIZE/2 + j * GRID_CELL_SIZE; + Coordinate p = new Coordinate(x, y); + Geometry geom = factory.createPoint(p).buffer(GRID_CELL_SIZE / 2.0); + geoms[index++] = geom; + } + } + return geoms; + } + + +} diff --git a/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonPointsPerfTest.java b/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonPointsPerfTest.java new file mode 100644 index 0000000000..8e924206db --- /dev/null +++ b/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonPointsPerfTest.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2022 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package test.jts.perf.operation.relateng; + +import static org.junit.Assert.assertEquals; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.prep.PreparedGeometry; +import org.locationtech.jts.geom.prep.PreparedGeometryFactory; +import org.locationtech.jts.geom.util.SineStarFactory; +import org.locationtech.jts.operation.relateng.RelateNG; +import org.locationtech.jts.operation.relateng.RelatePredicate; + +import test.jts.perf.PerformanceTestCase; +import test.jts.perf.PerformanceTestRunner; + +public class RelateNGPolygonPointsPerfTest +extends PerformanceTestCase +{ + + public static void main(String args[]) { + PerformanceTestRunner.run(RelateNGPolygonPointsPerfTest.class); + } + + private static final int N_ITER = 1; + + static double ORG_X = 100; + static double ORG_Y = ORG_X; + static double SIZE = 2 * ORG_X; + static int N_ARMS = 6; + static double ARM_RATIO = 0.3; + + static int GRID_SIZE = 100; + + private static GeometryFactory geomFact = new GeometryFactory(); + + private Geometry geomA; + private Geometry[] geomB; + + public RelateNGPolygonPointsPerfTest(String name) { + super(name); + setRunSize(new int[] { 100, 1000, 10000, 100000 }); + setRunIterations(N_ITER); + } + + public void setUp() + { + System.out.println("RelateNG perf test"); + System.out.println("SineStar: origin: (" + + ORG_X + ", " + ORG_Y + ") size: " + SIZE + + " # arms: " + N_ARMS + " arm ratio: " + ARM_RATIO); + System.out.println("# Iterations: " + N_ITER); + } + + public void startRun(int npts) + { + Geometry sineStar = SineStarFactory.create(new Coordinate(ORG_X, ORG_Y), SIZE, npts, N_ARMS, ARM_RATIO); + geomA = sineStar; + + geomB = createTestPoints(geomA.getEnvelopeInternal(), GRID_SIZE); + + System.out.println("\n------- Running with A: # pts = " + npts + + " B: " + geomB.length + " points"); + + /* + if (npts == 999) { + System.out.println(geomA); + + for (Geometry g : geomB) { + System.out.println(g); + } + } +*/ + } + + public void runIntersectsOld() + { + for (Geometry b : geomB) { + geomA.intersects(b); + } + } + + public void runIntersectsOldPrep() + { + PreparedGeometry pgA = PreparedGeometryFactory.prepare(geomA); + for (Geometry b : geomB) { + pgA.intersects(b); + } + } + + public void runIntersectsNG() + { + for (Geometry b : geomB) { + RelateNG.relate(geomA, b, RelatePredicate.intersects()); + } + } + + public void runIntersectsNGPrep() + { + RelateNG rng = RelateNG.prepare(geomA); + for (Geometry b : geomB) { + rng.evaluate(b, RelatePredicate.intersects()); + } + } + + public void runContainsOld() + { + for (Geometry b : geomB) { + geomA.contains(b); + } + } + + public void runContainsOldPrep() + { + PreparedGeometry pgA = PreparedGeometryFactory.prepare(geomA); + for (Geometry b : geomB) { + pgA.contains(b); + } + } + + public void runContainsNG() + { + for (Geometry b : geomB) { + RelateNG.relate(geomA, b, RelatePredicate.contains()); + } + } + + public void runContainsNGPrep() + { + RelateNG rng = RelateNG.prepare(geomA); + for (Geometry b : geomB) { + rng.evaluate(b, RelatePredicate.contains()); + } + } + + public void xrunContainsNGPrepValidate() + { + RelateNG rng = RelateNG.prepare(geomA); + for (Geometry b : geomB) { + boolean resultNG = rng.evaluate(b, RelatePredicate.contains()); + boolean resultOld = geomA.contains(b); + assertEquals(resultNG, resultOld); + } + } + + private Geometry[] createTestPoints(Envelope env, int nPtsOnSide) { + Geometry[] geoms = new Geometry[ nPtsOnSide * nPtsOnSide ]; + double baseX = env.getMinX(); + double deltaX = env.getWidth() / nPtsOnSide; + double baseY = env.getMinY(); + double deltaY = env.getHeight() / nPtsOnSide; + int index = 0; + for (int i = 0; i < nPtsOnSide; i++) { + for (int j = 0; j < nPtsOnSide; j++) { + double x = baseX + i * deltaX; + double y = baseY + i * deltaY; + Geometry geom = geomFact.createPoint(new Coordinate(x, y)); + geoms[index++] = geom; + } + } + return geoms; + } + + +} diff --git a/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonsAdjacentPerfTest.java b/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonsAdjacentPerfTest.java new file mode 100644 index 0000000000..c625666df9 --- /dev/null +++ b/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonsAdjacentPerfTest.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2022 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package test.jts.perf.operation.relateng; + +import java.io.FileReader; +import java.util.List; + +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.prep.PreparedGeometry; +import org.locationtech.jts.geom.prep.PreparedGeometryFactory; +import org.locationtech.jts.io.WKTFileReader; +import org.locationtech.jts.io.WKTReader; +import org.locationtech.jts.operation.relateng.IntersectionMatrixPattern; +import org.locationtech.jts.operation.relateng.RelateNG; +import org.locationtech.jts.operation.relateng.RelatePredicate; + +import test.jts.TestFiles; +import test.jts.perf.PerformanceTestCase; +import test.jts.perf.PerformanceTestRunner; + +public class RelateNGPolygonsAdjacentPerfTest +extends PerformanceTestCase +{ + + public static void main(String args[]) { + PerformanceTestRunner.run(RelateNGPolygonsAdjacentPerfTest.class); + } + + WKTReader rdr = new WKTReader(); + + private static final int N_ITER = 10; + + private List polygons; + + public RelateNGPolygonsAdjacentPerfTest(String name) { + super(name); + setRunSize(new int[] { 1 }); + //setRunSize(new int[] { 20 }); + setRunIterations(N_ITER); + } + + public void setUp() throws Exception + { + String resource = "europe.wkt"; + //String resource = "world.wkt"; + loadPolygons(resource); + + System.out.println("RelateNG Performance Test - Adjacent Polygons "); + System.out.println("Dataset: " + resource); + + System.out.println("# geometries: " + polygons.size() + + " # pts: " + numPts(polygons)); + System.out.println("----------------------------------"); + } + + private static int numPts(List geoms) { + int n = 0; + for (Geometry g : geoms) { + n += g.getNumPoints(); + } + return n; + } + + private void loadPolygons(String resourceName) throws Exception { + String path = TestFiles.getResourceFilePath(resourceName); + WKTFileReader wktFileRdr = new WKTFileReader(new FileReader(path), rdr); + polygons = wktFileRdr.read(); + } + + public void startRun(int npts) + { + + } + + public void runIntersectsOld() + { + for (Geometry a : polygons) { + for (Geometry b : polygons) { + a.intersects(b); + } + } + } + + public void runIntersectsOldPrep() + { + for (Geometry a : polygons) { + PreparedGeometry pgA = PreparedGeometryFactory.prepare(a); + for (Geometry b : polygons) { + pgA.intersects(b); + } + } + } + + public void runIntersectsNG() + { + for (Geometry a : polygons) { + for (Geometry b : polygons) { + RelateNG.relate(a, b, RelatePredicate.intersects()); + } + } + } + + public void runIntersectsNGPrep() + { + for (Geometry a : polygons) { + RelateNG rng = RelateNG.prepare(a); + for (Geometry b : polygons) { + rng.evaluate(b, RelatePredicate.intersects()); + } + } + } + + public void runTouchesOld() + { + for (Geometry a : polygons) { + for (Geometry b : polygons) { + a.touches(b); + } + } + } + + public void runTouchesNG() + { + for (Geometry a : polygons) { + for (Geometry b : polygons) { + RelateNG.relate(a, b, RelatePredicate.touches()); + } + } + } + + public void runTouchesNGPrep() + { + for (Geometry a : polygons) { + RelateNG rng = RelateNG.prepare(a); + for (Geometry b : polygons) { + rng.evaluate(b, RelatePredicate.touches()); + } + } + } + + public void runAdjacentOld() + { + for (Geometry a : polygons) { + for (Geometry b : polygons) { + a.relate(b, IntersectionMatrixPattern.ADJACENT); + } + } + } + + public void runAdjacentNG() + { + for (Geometry a : polygons) { + for (Geometry b : polygons) { + RelateNG.relate(a, b, RelatePredicate.matches(IntersectionMatrixPattern.ADJACENT)); + } + } + } + + public void runAdjacentNGPrep() + { + for (Geometry a : polygons) { + RelateNG rng = RelateNG.prepare(a); + for (Geometry b : polygons) { + rng.evaluate(b, RelatePredicate.matches(IntersectionMatrixPattern.ADJACENT)); + } + } + } + + public void runInteriorIntersectsOld() + { + for (Geometry a : polygons) { + for (Geometry b : polygons) { + a.relate(b, IntersectionMatrixPattern.INTERIOR_INTERSECTS); + } + } + } + + public void runInteriorIntersectsNG() + { + for (Geometry a : polygons) { + for (Geometry b : polygons) { + RelateNG.relate(a, b, RelatePredicate.matches(IntersectionMatrixPattern.INTERIOR_INTERSECTS)); + } + } + } + + public void runInteriorIntersectsNGPrep() + { + for (Geometry a : polygons) { + RelateNG rng = RelateNG.prepare(a); + for (Geometry b : polygons) { + rng.evaluate(b, RelatePredicate.matches(IntersectionMatrixPattern.INTERIOR_INTERSECTS)); + } + } + } + +} diff --git a/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonsOverlappingPerfTest.java b/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonsOverlappingPerfTest.java new file mode 100644 index 0000000000..d1a9a79777 --- /dev/null +++ b/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonsOverlappingPerfTest.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2022 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package test.jts.perf.operation.relateng; + +import static org.junit.Assert.assertEquals; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.prep.PreparedGeometry; +import org.locationtech.jts.geom.prep.PreparedGeometryFactory; +import org.locationtech.jts.geom.util.SineStarFactory; +import org.locationtech.jts.operation.relateng.RelateNG; +import org.locationtech.jts.operation.relateng.RelatePredicate; + +import test.jts.perf.PerformanceTestCase; +import test.jts.perf.PerformanceTestRunner; + +public class RelateNGPolygonsOverlappingPerfTest +extends PerformanceTestCase +{ + + public static void main(String args[]) { + PerformanceTestRunner.run(RelateNGPolygonsOverlappingPerfTest.class); + } + + private static final int N_ITER = 1; + + static double ORG_X = 100; + static double ORG_Y = ORG_X; + static double SIZE = 2 * ORG_X; + static int N_ARMS = 6; + static double ARM_RATIO = 0.3; + + static int GRID_SIZE = 100; + static double GRID_CELL_SIZE = SIZE / GRID_SIZE; + + static int NUM_CASES = GRID_SIZE * GRID_SIZE; + + private static final int B_SIZE_FACTOR = 20; + private static final GeometryFactory factory = new GeometryFactory(); + + private Geometry geomA; + + private Geometry[] geomB; + + public RelateNGPolygonsOverlappingPerfTest(String name) { + super(name); + setRunSize(new int[] { 100, 1000, 10000, 100000, + 200000 }); + //setRunSize(new int[] { 200000 }); + setRunIterations(N_ITER); + } + + public void setUp() + { + System.out.println("RelateNG perf test"); + System.out.println("SineStar: origin: (" + + ORG_X + ", " + ORG_Y + ") size: " + SIZE + + " # arms: " + N_ARMS + " arm ratio: " + ARM_RATIO); + System.out.println("# Iterations: " + N_ITER); + System.out.println("# B geoms: " + NUM_CASES); + } + + public void startRun(int npts) + { + Geometry sineStar = SineStarFactory.create(new Coordinate(ORG_X, ORG_Y), SIZE, npts, N_ARMS, ARM_RATIO); + geomA = sineStar; + + int nptsB = npts * B_SIZE_FACTOR / NUM_CASES; + if (nptsB < 10 ) nptsB = 10; + + geomB = createSineStarGrid(NUM_CASES, nptsB); + //geomB = createCircleGrid(NUM_CASES, nptsB); + + System.out.println("\n------- Running with A: polygon # pts = " + npts + + " B # pts = " + nptsB + " x " + NUM_CASES + " polygons"); + + /* + if (npts == 999) { + System.out.println(geomA); + + for (Geometry g : geomB) { + System.out.println(g); + } + } +*/ + } + + public void runIntersectsOld() + { + for (Geometry b : geomB) { + geomA.intersects(b); + } + } + + public void runIntersectsOldPrep() + { + PreparedGeometry pgA = PreparedGeometryFactory.prepare(geomA); + for (Geometry b : geomB) { + pgA.intersects(b); + } + } + + public void runIntersectsNG() + { + for (Geometry b : geomB) { + RelateNG.relate(geomA, b, RelatePredicate.intersects()); + } + } + + public void runIntersectsNGPrep() + { + RelateNG rng = RelateNG.prepare(geomA); + for (Geometry b : geomB) { + rng.evaluate(b, RelatePredicate.intersects()); + } + } + + public void runContainsOld() + { + for (Geometry b : geomB) { + geomA.contains(b); + } + } + + public void runContainsOldPrep() + { + PreparedGeometry pgA = PreparedGeometryFactory.prepare(geomA); + for (Geometry b : geomB) { + pgA.contains(b); + } + } + + public void runContainsNG() + { + for (Geometry b : geomB) { + RelateNG.relate(geomA, b, RelatePredicate.contains()); + } + } + + public void runContainsNGPrep() + { + RelateNG rng = RelateNG.prepare(geomA); + for (Geometry b : geomB) { + rng.evaluate(b, RelatePredicate.contains()); + } + } + + public void xrunContainsNGPrepValidate() + { + RelateNG rng = RelateNG.prepare(geomA); + for (Geometry b : geomB) { + boolean resultNG = rng.evaluate(b, RelatePredicate.contains()); + boolean resultOld = geomA.contains(b); + assertEquals(resultNG, resultOld); + } + } + + private Geometry[] createSineStarGrid(int nGeoms, int npts) { + Geometry[] geoms = new Geometry[ NUM_CASES ]; + int index = 0; + for (int i = 0; i < GRID_SIZE; i++) { + for (int j = 0; j < GRID_SIZE; j++) { + double x = GRID_CELL_SIZE/2 + i * GRID_CELL_SIZE; + double y = GRID_CELL_SIZE/2 + j * GRID_CELL_SIZE; + Geometry geom = SineStarFactory.create(new Coordinate(x, y), GRID_CELL_SIZE, npts, N_ARMS, ARM_RATIO); + geoms[index++] = geom; + } + } + return geoms; + } + + private Geometry[] createCircleGrid(int nGeoms, int npts) { + Geometry[] geoms = new Geometry[ NUM_CASES ]; + int index = 0; + for (int i = 0; i < GRID_SIZE; i++) { + for (int j = 0; j < GRID_SIZE; j++) { + double x = GRID_CELL_SIZE/2 + i * GRID_CELL_SIZE; + double y = GRID_CELL_SIZE/2 + j * GRID_CELL_SIZE; + Coordinate p = new Coordinate(x, y); + Geometry geom = factory.createPoint(p).buffer(GRID_CELL_SIZE / 2.0); + geoms[index++] = geom; + } + } + return geoms; + } + + +} diff --git a/modules/tests/src/test/resources/testxml/misc/TestRelateGC.xml b/modules/tests/src/test/resources/testxml/misc/TestRelateGC.xml new file mode 100644 index 0000000000..8eec76e4bc --- /dev/null +++ b/modules/tests/src/test/resources/testxml/misc/TestRelateGC.xml @@ -0,0 +1,537 @@ + + + + GC:L/GC:PL - a line with the same line in a collection with an empty polygon + + LINESTRING(0 0, 1 1) + + + GEOMETRYCOLLECTION(POLYGON EMPTY, LINESTRING(0 0, 1 1)) + + true + true + true + true + false + false + true + true + false + false + true + + + + A/GC:mP + + POLYGON((-60 -50,-70 -50,-60 -40,-60 -50)) + + + GEOMETRYCOLLECTION(MULTIPOINT((-60 -50),(-63 -49))) + + true + true + false + true + false + false + false + true + false + false + false + + + + mA/GC:PL + + MULTIPOLYGON (((0 0, 3 0, 3 3, 0 3, 0 0))) + + + GEOMETRYCOLLECTION ( LINESTRING (1 2, 1 1), POINT (0 0)) + + true + true + false + true + false + false + false + true + false + false + false + + + + GC:PL/mA + + GEOMETRYCOLLECTION (POINT (7 1), LINESTRING (6 5, 6 4)) + + + POLYGON ((7 1, 1 3, 3 9, 7 1)) + + true + false + false + false + false + false + false + true + false + true + false + + + + P/GC:PL - point on boundary of GC with line and point + + POINT(0 0) + + + GEOMETRYCOLLECTION(POINT(0 0), LINESTRING(0 0, 1 0)) + + true + false + true + false + false + false + false + true + false + true + false + + + + + L/GC:A - line in interior of GC of overlapping polygons + + LINESTRING (3 7, 7 3) + + + GEOMETRYCOLLECTION (POLYGON ((1 9, 7 9, 7 3, 1 3, 1 9)), POLYGON ((9 1, 3 1, 3 7, 9 7, 9 1))) + + true + false + true + false + false + false + false + true + false + false + true + + + + P/GC:A - point on common boundaries of 2 adjacent polygons + + POINT (4 3) + + + GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 4 6, 4 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1))) + + true + false + true + false + false + false + false + true + false + false + true + + + + P/GC:A - point on common node of 3 adjacent polygons + + POINT (5 4) + + + GEOMETRYCOLLECTION (POLYGON ((1 6, 5 4, 4 1, 1 6)), POLYGON ((4 1, 5 4, 9 6, 4 1)), POLYGON ((1 6, 9 6, 5 4, 1 6))) + + true + false + true + false + false + false + false + true + false + false + true + + + + P/GC:A - point on common node of 6 adjacent polygons, with holes at node + + POINT (6 6) + + +GEOMETRYCOLLECTION (POLYGON ((1 9, 5 9, 6 6, 1 5, 1 9), (2 6, 4 8, 6 6, 2 6)), POLYGON ((2 6, 4 8, 6 6, 2 6)), POLYGON ((9 9, 9 5, 6 6, 5 9, 9 9)), POLYGON ((9 1, 5 1, 6 6, 9 5, 9 1), (7 2, 6 6, 8 3, 7 2)), POLYGON ((7 2, 6 6, 8 3, 7 2)), POLYGON ((1 1, 1 5, 6 6, 5 1, 1 1))) + + true + false + true + false + false + false + false + true + false + false + true + + + + P/GC:A - point on common node of 5 adjacent polygons, with holes at node and one not filled + + POINT (6 6) + + +GEOMETRYCOLLECTION (POLYGON ((1 9, 5 9, 6 6, 1 5, 1 9), (2 6, 4 8, 6 6, 2 6)), POLYGON ((2 6, 4 8, 6 6, 2 6)), POLYGON ((9 9, 9 5, 6 6, 5 9, 9 9)), POLYGON ((9 1, 5 1, 6 6, 9 5, 9 1), (7 2, 6 6, 8 3, 7 2)), POLYGON ((1 1, 1 5, 6 6, 5 1, 1 1))) + + true + false + true + false + false + false + false + true + false + true + false + + + + L/GC:A - line on common boundaries of adjacent polygons + + LINESTRING (4 5, 4 2) + + + GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 4 6, 4 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1))) + + true + false + true + false + false + false + false + true + false + false + true + + + + L/GC:A - line on exterior boundaries of GC of overlapping polygons + + LINESTRING (2 6, 8 6) + + + GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 6, 6 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1))) + + true + false + true + false + false + false + false + true + false + true + false + + + + GC:L/GC:A - lines covers boundaries of overlapping polygons + + GEOMETRYCOLLECTION (LINESTRING (2 6, 9 6, 9 1, 7 1), LINESTRING (8 1, 1 1, 1 6, 7 6)) + + + GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 6, 6 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1))) + + true + false + true + false + false + false + false + true + false + true + false + + + + GC:A/GC:A - adjacent polygons contained by adjacent polygons + + GEOMETRYCOLLECTION (POLYGON ((2 2, 2 5, 4 5, 4 2, 2 2)), POLYGON ((8 2, 4 3, 4 4, 8 5, 8 2))) + + + GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 4 6, 4 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1))) + + true + false + true + false + false + false + false + true + false + false + true + + + + GC:A/P - adjacent polygons contain point at interior node + + GEOMETRYCOLLECTION (POLYGON ((5 5, 2 9, 9 9, 9 5, 5 5)), POLYGON ((3 1, 5 5, 9 5, 9 1, 3 1)), POLYGON ((1 9, 2 9, 5 5, 3 1, 1 1, 1 9))) + + + POINT (5 5) + + true + true + false + true + false + false + false + true + false + false + false + + + + GC:A/P - adjacent polygons contain point on interior edge + + GEOMETRYCOLLECTION (POLYGON ((5 5, 2 9, 9 9, 9 5, 5 5)), POLYGON ((3 1, 5 5, 9 5, 9 1, 3 1)), POLYGON ((1 9, 2 9, 5 5, 3 1, 1 1, 1 9))) + + + POINT (7 5) + + true + true + false + true + false + false + false + true + false + false + false + + + + GC:A/P - adjacent polygons cover point on exterior node + + GEOMETRYCOLLECTION (POLYGON ((5 5, 2 9, 9 9, 9 5, 5 5)), POLYGON ((3 1, 5 5, 9 5, 9 1, 3 1)), POLYGON ((1 9, 2 9, 5 5, 3 1, 1 1, 1 9))) + + + POINT (9 5) + + true + false + false + true + false + false + false + true + false + true + false + + + + GC:A/L - adjacent polygons contain line touching interior node + + GEOMETRYCOLLECTION (POLYGON ((5 5, 2 9, 9 9, 9 5, 5 5)), POLYGON ((3 1, 5 5, 9 5, 9 1, 3 1)), POLYGON ((1 9, 2 9, 5 5, 3 1, 1 1, 1 9))) + + + LINESTRING (5 5, 7 7) + + true + true + false + true + false + false + false + true + false + false + false + + + + GC:A/L - adjacent polygons contain line along interior edge to boundary + + GEOMETRYCOLLECTION (POLYGON ((5 5, 2 9, 9 9, 9 5, 5 5)), POLYGON ((3 1, 5 5, 9 5, 9 1, 3 1)), POLYGON ((1 9, 2 9, 5 5, 3 1, 1 1, 1 9))) + + + LINESTRING (5 5, 9 5) + + true + true + false + true + false + false + false + true + false + false + false + + + + GC:A/GC:PL - adjacent polygons contain line and point + + GEOMETRYCOLLECTION (POLYGON ((5 5, 2 9, 9 9, 9 5, 5 5)), POLYGON ((3 1, 5 5, 9 5, 9 1, 3 1)), POLYGON ((1 9, 2 9, 5 5, 3 1, 1 1, 1 9))) + + + GEOMETRYCOLLECTION (POINT (5 5), LINESTRING (5 7, 7 7)) + + true + true + false + true + false + false + false + true + false + false + false + + + + GC:A/A - adjacent polygons containing polygon with endpoint inside + + GEOMETRYCOLLECTION (POLYGON ((5 5, 2 9, 9 9, 9 5, 5 5)), POLYGON ((3 1, 5 5, 9 5, 9 1, 3 1)), POLYGON ((1 9, 2 9, 5 5, 3 1, 1 1, 1 9))) + + + POLYGON ((3 7, 7 7, 7 3, 3 3, 3 7)) + + true + true + false + true + false + false + false + true + false + false + false + + + + GC:A/A - adjacent polygons overlapping polygon with shell outside and hole inside + + GEOMETRYCOLLECTION (POLYGON ((5 5, 2 9, 9 9, 9 5, 5 5)), POLYGON ((3 1, 5 5, 9 5, 9 1, 3 1)), POLYGON ((1 9, 2 9, 5 5, 3 1, 1 1, 1 9))) + + + POLYGON ((0 10, 10 10, 10 0, 0 0, 0 10), (2 8, 8 8, 8 2, 2 2, 2 8)) + + true + false + false + false + false + false + false + true + true + false + false + + + + GC:A/GC:A - overlapping polygons equal to overlapping polygons + + GEOMETRYCOLLECTION (POLYGON ((1 6, 9 6, 9 2, 1 2, 1 6)), POLYGON ((9 1, 1 1, 1 5, 9 5, 9 1))) + + + GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 6, 6 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1))) + + true + true + true + true + false + false + true + true + false + false + true + + + + GC:A/GC:A - overlapping polygons contained by overlapping polygons + + GEOMETRYCOLLECTION (POLYGON ((4 4, 6 4, 6 3, 4 3, 4 4)), POLYGON ((2 5, 8 5, 8 2, 2 2, 2 5))) + + + GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 6, 6 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1))) + + true + false + true + false + false + false + false + true + false + false + true + + + + + A/GC:A - polygon equal to nested overlapping polygons + + POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9)) + + + GEOMETRYCOLLECTION ( + POLYGON ((1 1, 1 5, 5 5, 5 1, 1 1)), + GEOMETRYCOLLECTION( + POLYGON ((1 5, 5 9, 9 9, 9 5, 5 1, 1 5)), + MULTIPOLYGON (((1 9, 5 9, 5 5, 1 5, 1 9)), ((9 1, 5 1, 5 5, 9 5, 9 1))) + ) + ) + + true + true + true + true + false + false + true + true + false + false + true + + + + From b95572260e5201631959463c27a633d99d1ce6d5 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 20 May 2024 12:30:48 -0700 Subject: [PATCH 077/123] Javadoc --- .../org/locationtech/jts/operation/relateng/RelateNG.java | 6 +++++- .../jts/operation/relateng/TopologyPredicate.java | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java index 977fd75637..fa3ee65696 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java @@ -320,7 +320,11 @@ private void computeAtPoints(RelateGeometry geom, boolean isA, * if it has areas OR if the predicate requires checking for * exterior interaction. * In particular, this avoids testing line ends against lines - * for the intersects predicate. + * for the intersects predicate (since these are checked + * during segment/segment intersection checking anyway). + * Checking points against areas is necessary, since the input + * linework may be entirely disjoint if one input lies wholly + * inside an area. */ boolean checkDisjointPoints = geomTarget.hasDimension(Dimension.A) || topoComputer.isExteriorCheckRequired(isA); diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicate.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicate.java index 639427ede9..53f7750f20 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicate.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicate.java @@ -52,9 +52,13 @@ default boolean requiresSelfNoding() { /** * Reports whether this predicate requires checking if the source input intersects * the Exterior of the target input. - * This allows some performance optimizations if not required. + * This is the case if: + *
+   * IM[Int(Src), Ext(Tgt)] >= 0 or IM[Bdy(Src), Ext(Tgt)] >= 0
+   * 
+ * When not required to evaluate a predicate this permits performance optimization. * - * @param isSourceA + * @param isSourceA flag indicating which input geometry is the source * @return true if the predicate requires checking whether the source intersects the target exterior */ default boolean requiresExteriorCheck(boolean isSourceA) { From 20374b916027424920d77d5ce284f329a77ea325 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 20 May 2024 12:32:54 -0700 Subject: [PATCH 078/123] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 82f2c00a0c..1bfc1add21 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -31,6 +31,7 @@ Distributions for older JTS versions can be obtained at the * Add `ConcaveHull.alphaShape` function (#952) * Add `OffsetCurve` Joined mode (#956) * Add `PointLocation.isOnSegment` function (#1048) +* Addd `RelateNG` API for improved topological relationship functionality and performance (#1052) ### Functionality Improvements * Improve `TopologyPreservingSimplifier` to prevent edge-disjoint line collapse (#925) @@ -38,7 +39,7 @@ Distributions for older JTS versions can be obtained at the * Reduce buffer curve short fillet segments (#960) * Added ability to specify boundary for `LargestEmptyCircle` (#973) * Improve `DouglaPeuckerSimplifier` and `TopologyPreservingSimplifier` to handle ring endpoints (#1013) -* Add Angle.sinSnap and .cosSnap to avoid small errors, e.g. with buffer operations (#1016) +* Add `Angle` functions `sinSnap` and `cosSnap` to avoid small errors, e.g. with buffer operations (#1016) * Improve Buffer input simplification for rings (#1022) ### Bug Fixes From 88303d06f89ba97897a5a8f12380fda785d7d982 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 21 May 2024 14:49:42 -0700 Subject: [PATCH 079/123] Add CoveragePolygonValidator section performance optimization (#1053) --- .../jts/coverage/CoveragePolygon.java | 63 +++++++++ .../coverage/CoveragePolygonValidator.java | 126 ++++++++++-------- .../jts/coverage/CoverageRing.java | 9 ++ 3 files changed, 140 insertions(+), 58 deletions(-) create mode 100644 modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygon.java diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygon.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygon.java new file mode 100644 index 0000000000..84f00d458c --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygon.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.coverage; + +import org.locationtech.jts.algorithm.locate.IndexedPointInAreaLocator; +import org.locationtech.jts.algorithm.locate.PointOnGeometryLocator; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Location; +import org.locationtech.jts.geom.Polygon; + +class CoveragePolygon { + + private Polygon polygon; + private Envelope polyEnv; + IndexedPointInAreaLocator locator; + + public CoveragePolygon(Polygon poly) { + this.polygon = poly; + polyEnv = polygon.getEnvelopeInternal(); + } + + public boolean intersects(Envelope env) { + //-- test intersection explicitly to avoid expensive null check + //return polyEnv.intersects(env); + return ! (env.getMinX() > polyEnv.getMaxX() + || env.getMaxX() < polyEnv.getMinX() + || env.getMinY() > polyEnv.getMaxY() + || env.getMaxY() < polyEnv.getMinY()); + } + + public boolean contains(Coordinate p) { + //-- test intersection explicitly to avoid expensive null check + if (! intersects(p)) + return false; + PointOnGeometryLocator pia = getLocator(); + return Location.INTERIOR == pia.locate(p); + } + + private boolean intersects(Coordinate p) { + return ! (p.x > polyEnv.getMaxX() || + p.x < polyEnv.getMinX() || + p.y > polyEnv.getMaxY() || + p.y < polyEnv.getMinY()); + } + + private PointOnGeometryLocator getLocator() { + if (locator == null) { + locator = new IndexedPointInAreaLocator(polygon); + } + return locator; + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java index a84c69e33a..b8ec59bd99 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java @@ -85,7 +85,7 @@ * */ public class CoveragePolygonValidator { - + /** * Validates that a polygon is coverage-valid against the * surrounding polygons in a polygonal coverage. @@ -122,8 +122,7 @@ public static Geometry validate(Geometry targetPolygon, Geometry[] adjPolygons, private double gapWidth = 0.0; private GeometryFactory geomFactory; private Geometry[] adjGeoms; - private List adjPolygons; - private IndexedPointInAreaLocator[] adjPolygonLocators; + private List adjCovPolygons; /** * Create a new validator. @@ -158,8 +157,8 @@ public void setGapWidth(double gapWidth) { * @return a linear geometry containing the segments causing invalidity (if any) */ public Geometry validate() { - adjPolygons = extractPolygons(adjGeoms); - adjPolygonLocators = new IndexedPointInAreaLocator[adjPolygons.size()]; + List adjPolygons = extractPolygons(adjGeoms); + adjCovPolygons = toCoveragePolygons(adjPolygons); List targetRings = CoverageRing.createRings(targetGeom); List adjRings = CoverageRing.createRings(adjPolygons); @@ -177,6 +176,14 @@ public Geometry validate() { return createInvalidLines(targetRings); } + private List toCoveragePolygons(List polygons) { + List covPolys = new ArrayList(); + for (Polygon poly : polygons) { + covPolys.add(new CoveragePolygon(poly)); + } + return covPolys; + } + private void checkTargetRings(List targetRings, List adjRings, Envelope targetEnv) { markMatchedSegments(targetRings, adjRings, targetEnv); @@ -197,7 +204,8 @@ private void checkTargetRings(List targetRings, List * Do further checks to see if any of them are are invalid. */ markInvalidInteractingSegments(targetRings, adjRings, gapWidth); - markInvalidInteriorSegments(targetRings, adjPolygons); + markInvalidInteriorSegments(targetRings, adjCovPolygons); + //OLDmarkInvalidInteriorSegments(targetRings, adjPolygons); } private static List extractPolygons(Geometry[] geoms) { @@ -368,77 +376,79 @@ private void markInvalidInteractingSegments(List targetRings, List segSetMutInt.process(adjRings, detector); } + /** + * Stride is chosen experimentally to provide good performance + */ + private static final int RING_SECTION_STRIDE = 1000; + /** * Marks invalid target segments which are fully interior * to an adjacent polygon. * * @param targetRings the rings with segments to test - * @param adjPolygons the adjacent polygons + * @param adjCovPolygons the adjacent polygons */ - private void markInvalidInteriorSegments(List targetRings, List adjPolygons) { + private void markInvalidInteriorSegments(List targetRings, List adjCovPolygons) { for (CoverageRing ring : targetRings) { - for (int i = 0; i < ring.size() - 1; i++) { - //-- skip check for segments with known state. - if (ring.isKnown(i)) - continue; + int stride = RING_SECTION_STRIDE; + for (int i = 0; i < ring.size() - 1; i += stride) { + int iEnd = i + stride; + if (iEnd >= ring.size()) + iEnd = ring.size() - 1; - /** - * Check if vertex is in interior of an adjacent polygon. - * If so, the segments on either side are in the interior. - * Mark them invalid, unless they are already matched. - */ - Coordinate p = ring.getCoordinate(i); - if (isInteriorVertex(p, adjPolygons)) { - ring.markInvalid(i); - //-- previous segment may be interior (but may also be matched) - int iPrev = i == 0 ? ring.size() - 2 : i-1; - if (! ring.isKnown(iPrev)) - ring.markInvalid(iPrev); - } + markInvalidInteriorSection(ring, i, iEnd, adjCovPolygons); } } } - + /** - * Tests if a coordinate is in the interior of some adjacent polygon. - * Uses the cached Point-In-Polygon indexed locators, for performance. + * Marks invalid target segments in a section which are interior + * to an adjacent polygon. + * Processing a section at a time dramatically improves efficiency. + * Due to the coherent organization of polygon rings, + * sections usually have a high spatial locality. + * This means that sections typically intersect only a few or often no adjacent polygons. + * The section envelope can be computed and tested against adjacent polygon envelopes quickly. + * The section can be skipped entirely if it does not interact with any polygons. * - * @param p the coordinate to test - * @param adjPolygons the list of polygons - * @return true if the point is in the interior + * @param ring + * @param iStart + * @param iEnd + * @param adjPolygons */ - private boolean isInteriorVertex(Coordinate p, List adjPolygons) { - /** - * There should not be too many adjacent polygons, - * and hopefully not too many segments with unknown status - * so a linear scan should not be too inefficient - */ - //TODO: try a spatial index? - for (int i = 0; i < adjPolygons.size(); i++) { - Polygon adjPoly = adjPolygons.get(i); - - if (polygonContainsPoint(i, adjPoly, p)) - return true; + private void markInvalidInteriorSection(CoverageRing ring, int iStart, int iEnd, List adjPolygons) { + Envelope sectionEnv = ring.getEnvelope(iStart, iEnd); + //TODO: is it worth indexing polygons? + for (CoveragePolygon adjPoly : adjPolygons) { + if (adjPoly.intersects(sectionEnv)) { + //-- test vertices in section + for (int i = iStart; i < iEnd; i++) { + markInvalidInteriorSegment(ring, i, adjPoly); + } + } } - return false; - } - - private boolean polygonContainsPoint(int index, Polygon poly, Coordinate pt) { - if (! poly.getEnvelopeInternal().intersects(pt)) - return false; - PointOnGeometryLocator pia = getLocator(index, poly); - return Location.INTERIOR == pia.locate(pt); } - private PointOnGeometryLocator getLocator(int index, Polygon poly) { - IndexedPointInAreaLocator loc = adjPolygonLocators[index]; - if (loc == null) { - loc = new IndexedPointInAreaLocator(poly); - adjPolygonLocators[index] = loc; + private void markInvalidInteriorSegment(CoverageRing ring, int i, CoveragePolygon adjPoly) { + //-- skip check for segments with known state. + if (ring.isKnown(i)) + return; + + /** + * Check if vertex is in interior of an adjacent polygon. + * If so, the segments on either side are in the interior. + * Mark them invalid, unless they are already matched. + */ + Coordinate p = ring.getCoordinate(i); + if (adjPoly.contains(p)) { + ring.markInvalid(i); + //-- previous segment may be interior (but may also be matched) + int iPrev = i == 0 ? ring.size() - 2 : i-1; + if (! ring.isKnown(iPrev)) + ring.markInvalid(iPrev); } - return loc; } - + private Geometry createInvalidLines(List rings) { List lines = new ArrayList(); for (CoverageRing ring : rings) { diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRing.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRing.java index 82ce9dfc3a..479d9d3df0 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRing.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRing.java @@ -17,6 +17,7 @@ import org.locationtech.jts.algorithm.Orientation; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateArrays; +import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LineString; @@ -92,6 +93,14 @@ private CoverageRing(Coordinate[] pts, boolean isInteriorOnRight) { isMatched = new boolean[size() - 1]; } + public Envelope getEnvelope(int start, int end) { + Envelope env = new Envelope(); + for (int i = start; i < end; i++) { + env.expandToInclude(getCoordinate(i)); + } + return env; + } + /** * Reports if the ring has canonical orientation, * with the polygon interior on the right (shell is CW). From ac8cfa6a73ea993a060af975edd2a99e1ed4e28e Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 21 May 2024 14:51:17 -0700 Subject: [PATCH 080/123] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 1bfc1add21..d67b98a416 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -82,6 +82,7 @@ Distributions for older JTS versions can be obtained at the * Improve `HPRtree` performance (#1012) * Improve performance of noding and overlay via `HPRtree` (#1012) * Improve `DistanceOp` performance for Point-Point (#1049) +* Improve `CoveragePolygonValidator` via section performance optimization (#1053) # Version 1.19 From a025ba59a627f197d5f366d2422a6453811cabae Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 21 May 2024 19:58:47 -0700 Subject: [PATCH 081/123] CoveragePolygon code cleanup, renaming --- .../jts/coverage/CoveragePolygon.java | 19 +++++++++---------- .../coverage/CoveragePolygonValidator.java | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygon.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygon.java index 84f00d458c..9c3fe95440 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygon.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygon.java @@ -29,30 +29,29 @@ public CoveragePolygon(Polygon poly) { polyEnv = polygon.getEnvelopeInternal(); } - public boolean intersects(Envelope env) { + public boolean intersectsEnv(Envelope env) { //-- test intersection explicitly to avoid expensive null check - //return polyEnv.intersects(env); return ! (env.getMinX() > polyEnv.getMaxX() || env.getMaxX() < polyEnv.getMinX() || env.getMinY() > polyEnv.getMaxY() || env.getMaxY() < polyEnv.getMinY()); } - public boolean contains(Coordinate p) { + private boolean intersectsEnv(Coordinate p) { //-- test intersection explicitly to avoid expensive null check - if (! intersects(p)) - return false; - PointOnGeometryLocator pia = getLocator(); - return Location.INTERIOR == pia.locate(p); - } - - private boolean intersects(Coordinate p) { return ! (p.x > polyEnv.getMaxX() || p.x < polyEnv.getMinX() || p.y > polyEnv.getMaxY() || p.y < polyEnv.getMinY()); } + public boolean contains(Coordinate p) { + if (! intersectsEnv(p)) + return false; + PointOnGeometryLocator pia = getLocator(); + return Location.INTERIOR == pia.locate(p); + } + private PointOnGeometryLocator getLocator() { if (locator == null) { locator = new IndexedPointInAreaLocator(polygon); diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java index b8ec59bd99..c3418cfed6 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java @@ -420,7 +420,7 @@ private void markInvalidInteriorSection(CoverageRing ring, int iStart, int iEnd, Envelope sectionEnv = ring.getEnvelope(iStart, iEnd); //TODO: is it worth indexing polygons? for (CoveragePolygon adjPoly : adjPolygons) { - if (adjPoly.intersects(sectionEnv)) { + if (adjPoly.intersectsEnv(sectionEnv)) { //-- test vertices in section for (int i = iStart; i < iEnd; i++) { markInvalidInteriorSegment(ring, i, adjPoly); From eb195d06a2b56e532660d3ee9b2b2c8e472cf546 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 22 May 2024 09:16:44 -0700 Subject: [PATCH 082/123] CoveragePolygonValidator code cleanup --- .../locationtech/jts/coverage/CoveragePolygonValidator.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java index c3418cfed6..6e00488116 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java @@ -16,15 +16,12 @@ import java.util.List; import java.util.Map; -import org.locationtech.jts.algorithm.locate.IndexedPointInAreaLocator; -import org.locationtech.jts.algorithm.locate.PointOnGeometryLocator; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LineSegment; import org.locationtech.jts.geom.LineString; -import org.locationtech.jts.geom.Location; import org.locationtech.jts.geom.Polygon; import org.locationtech.jts.geom.util.PolygonExtracter; import org.locationtech.jts.noding.MCIndexSegmentSetMutualIntersector; @@ -176,7 +173,7 @@ public Geometry validate() { return createInvalidLines(targetRings); } - private List toCoveragePolygons(List polygons) { + private static List toCoveragePolygons(List polygons) { List covPolys = new ArrayList(); for (Polygon poly : polygons) { covPolys.add(new CoveragePolygon(poly)); From e751aa211fc606a4dd93a28bf90e9c1dcd7b2fa7 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 22 May 2024 15:22:25 -0700 Subject: [PATCH 083/123] Improve JtsOp doc --- .../src/main/java/org/locationtech/jtstest/cmd/JTSOpCmd.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/cmd/JTSOpCmd.java b/modules/app/src/main/java/org/locationtech/jtstest/cmd/JTSOpCmd.java index 31ba6a307e..6f09f88476 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/cmd/JTSOpCmd.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/cmd/JTSOpCmd.java @@ -172,8 +172,8 @@ private static CommandLine createCmdLine() { " -eachb execute op on each element of B", " -index index the B geometries", " -repeat repeat the operation N times", - " -where op v output geometry where operation result matches predicate op and value.", - " Predicates ops are: eq, ne, ge, gt, le, lt", + " -where cond v output geometry where operation result matches condition and value.", + " Conditions are: eq, ne, ge, gt, le, lt", " -validate validate the result of each operation", " -geomfunc specifies class providing geometry operations", " -op separator to delineate operation arguments", From 3d75eb1ded58123eec8e791667e0b28707f0f687 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 27 May 2024 10:22:14 -0700 Subject: [PATCH 084/123] Add OrientationFP functions --- .../function/OrientationFPFunctions.java | 41 +++++++++++++++++++ .../GeometryFunctionRegistry.java | 2 + 2 files changed, 43 insertions(+) create mode 100644 modules/app/src/main/java/org/locationtech/jtstest/function/OrientationFPFunctions.java diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/OrientationFPFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/OrientationFPFunctions.java new file mode 100644 index 0000000000..a0c8ed77b5 --- /dev/null +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/OrientationFPFunctions.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2017 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jtstest.function; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; + +public class OrientationFPFunctions { + + public static int orientationIndex(Geometry segment, Geometry ptGeom) { + if (segment.getNumPoints() != 2 || ptGeom.getNumPoints() != 1) { + throw new IllegalArgumentException("A must have two points and B must have one"); + } + Coordinate[] segPt = segment.getCoordinates(); + + Coordinate p = ptGeom.getCoordinate(); + int index = orientationIndex(segPt[0], segPt[1], p); + return index; + } + + private static int orientationIndex(Coordinate p1, Coordinate p2, Coordinate q) + { + double dx1 = p2.x - p1.x; + double dy1 = p2.y - p1.y; + double dx2 = q.x - p2.x; + double dy2 = q.y - p2.y; + double det = dx1*dy2 - dx2*dy1; + if (det > 0.0) return 1; + if (det < 0.0) return -1; + return 0; + } +} diff --git a/modules/app/src/main/java/org/locationtech/jtstest/geomfunction/GeometryFunctionRegistry.java b/modules/app/src/main/java/org/locationtech/jtstest/geomfunction/GeometryFunctionRegistry.java index 319bfb67a3..c7f02cc2a2 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/geomfunction/GeometryFunctionRegistry.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/geomfunction/GeometryFunctionRegistry.java @@ -45,6 +45,7 @@ import org.locationtech.jtstest.function.LinearReferencingFunctions; import org.locationtech.jtstest.function.NodingFunctions; import org.locationtech.jtstest.function.OffsetCurveFunctions; +import org.locationtech.jtstest.function.OrientationFPFunctions; import org.locationtech.jtstest.function.OrientationFunctions; import org.locationtech.jtstest.function.OverlayFunctions; import org.locationtech.jtstest.function.OverlayNGFunctions; @@ -120,6 +121,7 @@ public static GeometryFunctionRegistry createTestBuilderRegistry() //funcRegistry.add(MemoryFunctions.class); funcRegistry.add(OffsetCurveFunctions.class); funcRegistry.add(OrientationFunctions.class); + funcRegistry.add(OrientationFPFunctions.class); funcRegistry.add(LineSegmentFunctions.class); funcRegistry.add(OverlayFunctions.class); From c1c8310f8c992db625d0c6b4d63da53c1b463fc8 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 27 May 2024 17:52:26 -0700 Subject: [PATCH 085/123] Add TestBuilder Sort by Num Points --- .../testbuilder/GeometryTreeModel.java | 17 ++++++++++++ .../jtstest/testbuilder/InspectorPanel.java | 26 +++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryTreeModel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryTreeModel.java index cfcb246a1e..3da5216c11 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryTreeModel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryTreeModel.java @@ -41,6 +41,8 @@ public class GeometryTreeModel implements TreeModel public static Comparator SORT_AREA_DESC = new AreaComparator(true); public static Comparator SORT_LEN_ASC = new LengthComparator(false); public static Comparator SORT_LEN_DESC = new LengthComparator(true); + public static Comparator SORT_NUMPTS_ASC = new NumPointsComparator(false); + public static Comparator SORT_NUMPTS_DESC = new NumPointsComparator(true); private Vector treeModelListeners = new Vector(); @@ -153,6 +155,21 @@ public int compare(GeometricObjectNode o1, GeometricObjectNode o2) { return dirFactor * Double.compare(area1, area2); } } + public static class NumPointsComparator implements Comparator { + + private int dirFactor; + + public NumPointsComparator(boolean direction) { + this.dirFactor = direction ? 1 : -1; + } + + @Override + public int compare(GeometricObjectNode o1, GeometricObjectNode o2) { + int num1 = o1.getGeometry().getNumPoints(); + int num2 = o2.getGeometry().getNumPoints(); + return dirFactor * Integer.compare(num1, num2); + } + } } abstract class GeometricObjectNode diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/InspectorPanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/InspectorPanel.java index f008b80707..c80849590a 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/InspectorPanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/InspectorPanel.java @@ -47,8 +47,8 @@ public class InspectorPanel extends TestBuilderPanel { private Geometry geometry; private Comparator sorterArea; - private Comparator sorterLen; + private Comparator sorterNumPoints; public InspectorPanel() { this(true); @@ -128,6 +128,11 @@ public void actionPerformed(ActionEvent e) { sortByLen(); } }); + JButton btnSortByNumPts = SwingUtil.createButton(AppIcons.ICON_POINT, "Sort by Num Points (Asc/Desc)", new java.awt.event.ActionListener() { + public void actionPerformed(ActionEvent e) { + sortByNumPoints(); + } + }); JPanel btn2Panel = new JPanel(); btn2Panel.setLayout(new BoxLayout(btn2Panel, BoxLayout.PAGE_AXIS)); @@ -148,6 +153,7 @@ public void actionPerformed(ActionEvent e) btn2Panel.add(Box.createRigidArea(new Dimension(0, 10))); btn2Panel.add(new JLabel("Sort")); + btn2Panel.add(btnSortByNumPts); btn2Panel.add(btnSortByLen); btn2Panel.add(btnSortByArea); btn2Panel.add(btnSortNone); @@ -214,12 +220,14 @@ public void sortNone() { sorterLen = null; sorterArea = null; + sorterNumPoints = null; geomTreePanel.populate(geometry, source); } public void sortByArea() { sorterLen = null; + sorterNumPoints = null; if (sorterArea == GeometryTreeModel.SORT_AREA_ASC) { sorterArea = GeometryTreeModel.SORT_AREA_DESC; @@ -233,6 +241,8 @@ public void sortByArea() public void sortByLen() { sorterArea = null; + sorterNumPoints = null; + if (sorterLen == GeometryTreeModel.SORT_LEN_ASC) { sorterLen = GeometryTreeModel.SORT_LEN_DESC; } @@ -242,6 +252,18 @@ public void sortByLen() geomTreePanel.populate(geometry, source, sorterLen); } - + public void sortByNumPoints() + { + sorterArea = null; + sorterLen = null; + + if (sorterNumPoints == GeometryTreeModel.SORT_NUMPTS_ASC) { + sorterNumPoints = GeometryTreeModel.SORT_NUMPTS_DESC; + } + else { + sorterNumPoints = GeometryTreeModel.SORT_NUMPTS_ASC; + } + geomTreePanel.populate(geometry, source, sorterNumPoints); + } } From a9baabaf64dab8ffb09c14c3a686f951af401648 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Sat, 1 Jun 2024 09:49:47 -0700 Subject: [PATCH 086/123] Add RelateNG envelope interaction/covers optimizations (#1055) --- .../jtstest/function/SelectionFunctions.java | 19 +++++++ .../function/SelectionNGFunctions.java | 51 ++++++++++++++++++- .../operation/relateng/IMPatternMatcher.java | 19 ++++--- .../jts/operation/relateng/IMPredicate.java | 13 +++++ .../operation/relateng/RelateGeometry.java | 4 +- .../relateng/RelateMatrixPredicate.java | 6 +++ .../jts/operation/relateng/RelateNG.java | 33 ++++++++++-- .../operation/relateng/RelatePredicate.java | 44 ++++++++++++---- .../operation/relateng/TopologyComputer.java | 4 +- .../operation/relateng/TopologyPredicate.java | 41 +++++++++++++-- .../relateng/TopologyPredicateTracer.java | 13 +++-- .../jts/operation/relateng/RelateNGTest.java | 2 +- 12 files changed, 217 insertions(+), 32 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionFunctions.java index 0545649fc9..047c5e48e7 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionFunctions.java @@ -70,6 +70,15 @@ public boolean isTrue(Geometry g) { }); } + public static Geometry touches(Geometry a, final Geometry mask) + { + return select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return mask.touches(g); + } + }); + } + public static Geometry disjoint(Geometry a, Geometry mask) { List selected = new ArrayList(); @@ -81,6 +90,16 @@ public static Geometry disjoint(Geometry a, Geometry mask) } return a.getFactory().buildGeometry(selected); } + + public static Geometry relatePattern(Geometry a, final Geometry mask, String pattern) + { + return select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return mask.relate(g, pattern); + } + }); + } + public static Geometry valid(Geometry a) { List selected = new ArrayList(); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionNGFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionNGFunctions.java index ab2bf55dfc..c571e0b3f5 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionNGFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionNGFunctions.java @@ -12,6 +12,7 @@ package org.locationtech.jtstest.function; +import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.operation.relateng.IntersectionMatrixPattern; import org.locationtech.jts.operation.relateng.RelateNG; @@ -31,9 +32,12 @@ public boolean isTrue(Geometry g) { public static Geometry intersectsPrep(Geometry a, final Geometry mask) { RelateNG relateNG = RelateNG.prepare(mask); + Envelope maskEnv = mask.getEnvelopeInternal(); return SelectionFunctions.select(a, new GeometryPredicate() { public boolean isTrue(Geometry g) { - return relateNG.evaluate(g, RelatePredicate.intersects()); + if (maskEnv.disjoint(g.getEnvelopeInternal())) + return false; + return relateNG.evaluate(g, RelatePredicate.intersects()); } }); } @@ -59,13 +63,38 @@ public boolean isTrue(Geometry g) { public static Geometry coversPrep(Geometry a, final Geometry mask) { RelateNG relateNG = RelateNG.prepare(mask); + Envelope maskEnv = mask.getEnvelopeInternal(); return SelectionFunctions.select(a, new GeometryPredicate() { public boolean isTrue(Geometry g) { + if (maskEnv.disjoint(g.getEnvelopeInternal())) + return false; return relateNG.evaluate(g, RelatePredicate.covers()); } }); } + public static Geometry touches(Geometry a, final Geometry mask) + { + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return RelateNG.relate(mask, g, RelatePredicate.touches()); + } + }); + } + + public static Geometry touchesPrep(Geometry a, final Geometry mask) + { + RelateNG relateNG = RelateNG.prepare(mask); + Envelope maskEnv = mask.getEnvelopeInternal(); + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + if (maskEnv.disjoint(g.getEnvelopeInternal())) + return false; + return relateNG.evaluate(g, RelatePredicate.touches()); + } + }); + } + public static Geometry adjacent(Geometry a, final Geometry mask) { return SelectionFunctions.select(a, new GeometryPredicate() { @@ -84,6 +113,26 @@ public boolean isTrue(Geometry g) { } }); } + + public static Geometry relatePattern(Geometry a, final Geometry mask, String pattern) + { + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return RelateNG.relate(mask, g, RelatePredicate.matches(pattern)); + } + }); + } + + public static Geometry relatePatternPrep(Geometry a, final Geometry mask, String pattern) + { + RelateNG relateNG = RelateNG.prepare(mask); + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return relateNG.evaluate(g, RelatePredicate.matches(pattern)); + } + }); + } + } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPatternMatcher.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPatternMatcher.java index 920dc4d124..ea93e3ac4d 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPatternMatcher.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPatternMatcher.java @@ -48,21 +48,26 @@ public IMPatternMatcher(String imPattern) { public void init(Envelope envA, Envelope envB) { super.init(dimA, dimB); //-- if pattern specifies any non-E/non-E interaction, envelopes must not be disjoint - boolean requiresInteraction = requiresInteraction(patternMatrix); + boolean requiresInteraction = requireInteraction(patternMatrix); boolean isDisjoint = envA.disjoint(envB); setValueIf(false, requiresInteraction && isDisjoint); } - private static boolean requiresInteraction(IntersectionMatrix im) { + @Override + public boolean requireInteraction() { + return requireInteraction(patternMatrix); + } + + private static boolean requireInteraction(IntersectionMatrix im) { boolean requiresInteraction = - requiresInteraction(im.get(Location.INTERIOR, Location.INTERIOR)) - || requiresInteraction(im.get(Location.INTERIOR, Location.BOUNDARY)) - || requiresInteraction(im.get(Location.BOUNDARY, Location.INTERIOR)) - || requiresInteraction(im.get(Location.BOUNDARY, Location.BOUNDARY)); + isInteraction(im.get(Location.INTERIOR, Location.INTERIOR)) + || isInteraction(im.get(Location.INTERIOR, Location.BOUNDARY)) + || isInteraction(im.get(Location.BOUNDARY, Location.INTERIOR)) + || isInteraction(im.get(Location.BOUNDARY, Location.BOUNDARY)); return requiresInteraction; } - private static boolean requiresInteraction(int imDim) { + private static boolean isInteraction(int imDim) { return imDim == Dimension.TRUE || imDim >= Dimension.P; } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPredicate.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPredicate.java index 9ac1f27f3e..1905e305b5 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPredicate.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPredicate.java @@ -77,6 +77,13 @@ public boolean isDimChanged(int locA, int locB, int dimension) { */ protected abstract boolean isDetermined(); + /** + * Tests whether the exterior of the specified input geometry + * is intersected by any part of the other input. + * + * @param isA the input geometry + * @return true if the input geometry exterior is intersected + */ protected boolean intersectsExteriorOf(boolean isA) { if (isA) { return isIntersects(Location.EXTERIOR, Location.INTERIOR) @@ -112,6 +119,12 @@ public void finish() { setValue(valueIM()); } + /** + * Gets the value of the predicate according to the current + * intersection matrix state. + * + * @return the current predicate value + */ protected abstract boolean valueIM(); public String toString() { diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateGeometry.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateGeometry.java index 005ddd6184..57ad0ef5ae 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateGeometry.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateGeometry.java @@ -48,6 +48,7 @@ public static String name(boolean isA) { private Geometry geom; private boolean isPrepared = false; + private Envelope geomEnv; private int geomDim = Dimension.FALSE; private Set uniquePoints; private BoundaryNodeRule boundaryNodeRule; @@ -69,6 +70,7 @@ public RelateGeometry(Geometry input, BoundaryNodeRule bnRule) { public RelateGeometry(Geometry input, boolean isPrepared, BoundaryNodeRule bnRule) { this.geom = input; + this.geomEnv = input.getEnvelopeInternal(); this.isPrepared = isPrepared; this.boundaryNodeRule = bnRule; //-- cache geometry metadata @@ -160,7 +162,7 @@ public boolean isPrepared() { } public Envelope getEnvelope() { - return geom.getEnvelopeInternal(); + return geomEnv; } public int getDimension() { diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateMatrixPredicate.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateMatrixPredicate.java index 1e224c0221..6a456b1b73 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateMatrixPredicate.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateMatrixPredicate.java @@ -25,6 +25,12 @@ public RelateMatrixPredicate() { public String name() { return "relateMatrix"; } + @Override + public boolean requireInteraction() { + //-- ensure entire matrix is computed + return false; + } + @Override public boolean isDetermined() { //-- ensure entire matrix is computed diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java index fa3ee65696..18c6ed3f38 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java @@ -220,7 +220,11 @@ public boolean evaluate(Geometry b, String imPattern) { * @return true if the predicate is satisfied */ public boolean evaluate(Geometry b, TopologyPredicate predicate) { - + //-- fast envelope checks + if (! hasRequiredEnvelopeInteraction(b, predicate)) { + return false; + } + RelateGeometry geomB = new RelateGeometry(b, boundaryNodeRule); if (geomA.isEmpty() && geomB.isEmpty()) { @@ -267,6 +271,29 @@ public boolean evaluate(Geometry b, TopologyPredicate predicate) { return topoComputer.getResult(); } + private boolean hasRequiredEnvelopeInteraction(Geometry b, TopologyPredicate predicate) { + Envelope envB = b.getEnvelopeInternal(); + boolean isInteracts = false; + if (predicate.requireCovers(GEOM_A)) { + if (! geomA.getEnvelope().covers(envB)) { + return false; + } + isInteracts = true; + } + else if (predicate.requireCovers(GEOM_B)) { + if (! envB.covers(geomA.getEnvelope())) { + return false; + } + isInteracts = true; + } + if (! isInteracts + && predicate.requireInteraction() + && ! geomA.getEnvelope().intersects(envB)) { + return false; + } + return true; + } + private boolean finishValue(TopologyPredicate predicate) { predicate.finish(); return predicate.value(); @@ -323,8 +350,8 @@ private void computeAtPoints(RelateGeometry geom, boolean isA, * for the intersects predicate (since these are checked * during segment/segment intersection checking anyway). * Checking points against areas is necessary, since the input - * linework may be entirely disjoint if one input lies wholly - * inside an area. + * linework is disjoint if one input lies wholly inside an area, + * so segment intersection checking is not sufficient. */ boolean checkDisjointPoints = geomTarget.hasDimension(Dimension.A) || topoComputer.isExteriorCheckRequired(isA); diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePredicate.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePredicate.java index 6eef9ba41f..87d9f51b53 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePredicate.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePredicate.java @@ -52,13 +52,13 @@ public static TopologyPredicate intersects() { public String name() { return "intersects"; } @Override - public boolean requiresSelfNoding() { + public boolean requireSelfNoding() { //-- self-noding is not required to check for a simple interaction return false; } @Override - public boolean requiresExteriorCheck(boolean isSourceA) { + public boolean requireExteriorCheck(boolean isSourceA) { //-- intersects only requires testing interaction return false; } @@ -104,13 +104,19 @@ public static TopologyPredicate disjoint() { public String name() { return "disjoint"; } @Override - public boolean requiresSelfNoding() { + public boolean requireSelfNoding() { //-- self-noding is not required to check for a simple interaction return false; } - + + @Override + public boolean requireInteraction() { + //-- ensure entire matrix is computed + return false; + } + @Override - public boolean requiresExteriorCheck(boolean isSourceA) { + public boolean requireExteriorCheck(boolean isSourceA) { //-- disjoint only requires testing interaction return false; } @@ -164,7 +170,12 @@ public static TopologyPredicate contains() { public String name() { return "contains"; } @Override - public boolean requiresExteriorCheck(boolean isSourceA) { + public boolean requireCovers(boolean isSourceA) { + return isSourceA == RelateGeometry.GEOM_A; + } + + @Override + public boolean requireExteriorCheck(boolean isSourceA) { //-- only need to check B against Exterior of A return isSourceA == RelateGeometry.GEOM_B; } @@ -222,7 +233,12 @@ public static TopologyPredicate within() { public String name() { return "within"; } @Override - public boolean requiresExteriorCheck(boolean isSourceA) { + public boolean requireCovers(boolean isSourceA) { + return isSourceA == RelateGeometry.GEOM_B; + } + + @Override + public boolean requireExteriorCheck(boolean isSourceA) { //-- only need to check A against Exterior of B return isSourceA == RelateGeometry.GEOM_A; } @@ -286,7 +302,12 @@ public static TopologyPredicate covers() { public String name() { return "covers"; } @Override - public boolean requiresExteriorCheck(boolean isSourceA) { + public boolean requireCovers(boolean isSourceA) { + return isSourceA == RelateGeometry.GEOM_A; + } + + @Override + public boolean requireExteriorCheck(boolean isSourceA) { //-- only need to check B against Exterior of A return isSourceA == RelateGeometry.GEOM_B; } @@ -345,7 +366,12 @@ public static TopologyPredicate coveredBy() { public String name() { return "coveredBy"; } @Override - public boolean requiresExteriorCheck(boolean isSourceA) { + public boolean requireCovers(boolean isSourceA) { + return isSourceA == RelateGeometry.GEOM_B; + } + + @Override + public boolean requireExteriorCheck(boolean isSourceA) { //-- only need to check A against Exterior of B return isSourceA == RelateGeometry.GEOM_A; } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyComputer.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyComputer.java index 1fd7d81880..de27064c94 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyComputer.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyComputer.java @@ -133,11 +133,11 @@ public boolean isSelfNodingRequired() { //TODO: change to testing for lines or GC with > 1 polygon if (geomA.isPointsOrPolygons()) return false; if (geomB.isPointsOrPolygons()) return false; - return predicate.requiresSelfNoding(); + return predicate.requireSelfNoding(); } public boolean isExteriorCheckRequired(boolean isA) { - return predicate.requiresExteriorCheck(isA); + return predicate.requireExteriorCheck(isA); } private void updateDim(int locA, int locB, int dimension) { diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicate.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicate.java index 53f7750f20..daf183c0fb 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicate.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicate.java @@ -45,10 +45,43 @@ public interface TopologyPredicate { * * @return true if self-noding is required. */ - default boolean requiresSelfNoding() { + default boolean requireSelfNoding() { return true; } + /** + * Reports whether this predicate requires interaction between + * the input geometries. + * This is the case if + *
+   * IM[I, I] >= 0 or IM[I, B] >= 0 or IM[B, I] >= 0 or IM[B, B] >= 0
+   * 
+ * This allows a fast result if + * the envelopes of the geometries are disjoint. + * + * @return true if the geometries must interact + */ + default boolean requireInteraction() { + return true; + } + + /** + * Reports whether this predicate requires that the source + * cover the target. + * This is the case if + *
+   * IM[Ext(Src), Int(Tgt)] = F and IM[Ext(Src), Bdy(Tgt)] = F
+   * 
+ * If true, this allows a fast result if + * the source envelope does not cover the target envelope. + * + * @param isSourceA indicates the source input geometry + * @return true if the predicate requires checking whether the source covers the target + */ + default boolean requireCovers(boolean isSourceA) { + return false; + } + /** * Reports whether this predicate requires checking if the source input intersects * the Exterior of the target input. @@ -56,12 +89,12 @@ default boolean requiresSelfNoding() { *
    * IM[Int(Src), Ext(Tgt)] >= 0 or IM[Bdy(Src), Ext(Tgt)] >= 0
    * 
- * When not required to evaluate a predicate this permits performance optimization. + * If false, this may permit a faster result in some geometric situations. * - * @param isSourceA flag indicating which input geometry is the source + * @param isSourceA indicates the source input geometry * @return true if the predicate requires checking whether the source intersects the target exterior */ - default boolean requiresExteriorCheck(boolean isSourceA) { + default boolean requireExteriorCheck(boolean isSourceA) { return true; } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateTracer.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateTracer.java index 416e21a799..1fb3f69cb8 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateTracer.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateTracer.java @@ -47,13 +47,18 @@ private PredicateTracer(TopologyPredicate pred) { public String name() { return pred.name(); } @Override - public boolean requiresSelfNoding() { - return pred.requiresSelfNoding(); + public boolean requireSelfNoding() { + return pred.requireSelfNoding(); } @Override - public boolean requiresExteriorCheck(boolean isSourceA) { - return pred.requiresExteriorCheck(isSourceA); + public boolean requireCovers(boolean isSourceA) { + return pred.requireCovers(isSourceA); + } + + @Override + public boolean requireExteriorCheck(boolean isSourceA) { + return pred.requireExteriorCheck(isSourceA); } @Override diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTest.java index 1ba16d1c63..cb793b1c8f 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTest.java @@ -23,7 +23,7 @@ public RelateNGTest(String name) { super(name); } - public void testDisjoint() { + public void testPointsDisjoint() { String a = "POINT (0 0)"; String b = "POINT (1 1)"; checkIntersectsDisjoint(a, b, false); From c404675e551e1faa6b95c5c50682ba8a5346d315 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Sat, 1 Jun 2024 09:50:15 -0700 Subject: [PATCH 087/123] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index d67b98a416..f05de43411 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -31,7 +31,7 @@ Distributions for older JTS versions can be obtained at the * Add `ConcaveHull.alphaShape` function (#952) * Add `OffsetCurve` Joined mode (#956) * Add `PointLocation.isOnSegment` function (#1048) -* Addd `RelateNG` API for improved topological relationship functionality and performance (#1052) +* Addd `RelateNG` API for improved topological relationship functionality and performance (#1052, #1055) ### Functionality Improvements * Improve `TopologyPreservingSimplifier` to prevent edge-disjoint line collapse (#925) From 640d091daf1f9d30b9f0a8eb9bfc2e393d368967 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Sat, 1 Jun 2024 09:55:43 -0700 Subject: [PATCH 088/123] Code cleanup --- .../jts/operation/relateng/EdgeSetIntersector.java | 12 +++++------- .../jts/operation/relateng/RelateEdge.java | 4 ---- .../jts/operation/relateng/RelateNode.java | 2 -- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSetIntersector.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSetIntersector.java index ab6a7c00bf..5e4cc40b15 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSetIntersector.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSetIntersector.java @@ -47,9 +47,8 @@ private void addEdges(Collection segStrings) private void addToIndex(SegmentString segStr) { - List segChains = MonotoneChainBuilder.getChains(segStr.getCoordinates(), segStr); - for (Iterator i = segChains.iterator(); i.hasNext(); ) { - MonotoneChain mc = (MonotoneChain) i.next(); + List segChains = MonotoneChainBuilder.getChains(segStr.getCoordinates(), segStr); + for (MonotoneChain mc : segChains ) { if (envelope == null || envelope.intersects(mc.getEnvelope())) { mc.setId(idCounter ++); index.insert(mc.getEnvelope(), mc); @@ -62,10 +61,9 @@ public void process(EdgeSegmentIntersector intersector) { MonotoneChainOverlapAction overlapAction = new EdgeSegmentOverlapAction(intersector); for (MonotoneChain queryChain : monoChains) { - List overlapChains = index.query(queryChain.getEnvelope()); - for (Iterator j = overlapChains.iterator(); j.hasNext(); ) { - MonotoneChain testChain = (MonotoneChain) j.next(); - /** + List overlapChains = index.query(queryChain.getEnvelope()); + for (MonotoneChain testChain : overlapChains) { + /** * following test makes sure we only compare each pair of chains once * and that we don't compare a chain to itself */ diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateEdge.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateEdge.java index e9cd8f7113..0a2e0bf5fb 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateEdge.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateEdge.java @@ -59,10 +59,6 @@ public static void setAreaInterior(List edges, boolean isA) { */ private static int LOC_UNKNOWN = Location.NONE; - private static boolean isKnown(int loc) { - return loc != LOC_UNKNOWN; - } - private RelateNode node; private Coordinate dirPt; diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNode.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNode.java index 92bfd8cbe2..7c3ff91f8b 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNode.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNode.java @@ -19,8 +19,6 @@ import org.locationtech.jts.geom.Location; import org.locationtech.jts.geom.Position; import org.locationtech.jts.io.WKTWriter; -import org.locationtech.jts.util.Assert; -import org.locationtech.jts.util.Debug; class RelateNode { From a7927da64b84aa03b58080ef6f4d0799f0cdb9a3 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 3 Jun 2024 19:46:35 -0700 Subject: [PATCH 089/123] Add RelatePredicate unit test --- .../relateng/RelatePredicateTest.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelatePredicateTest.java diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelatePredicateTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelatePredicateTest.java new file mode 100644 index 0000000000..f56f3c3544 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelatePredicateTest.java @@ -0,0 +1,81 @@ +package org.locationtech.jts.operation.relateng; + +import org.locationtech.jts.geom.Dimension; + +import junit.framework.TestCase; +import junit.textui.TestRunner; + +public class RelatePredicateTest extends TestCase { + + private static final String A_EXT_B_INT = "***.***.1**"; + private static final String A_INT_B_INT = "1**.***.***"; + + public static void main(String args[]) { + TestRunner.run(RelatePredicateTest.class); + } + + public RelatePredicateTest(String name) { + super(name); + } + + public void testIntersects() { + checkPredicate(RelatePredicate.intersects(), A_INT_B_INT, true); + } + + public void testDisjoint() { + checkPredicate(RelatePredicate.intersects(), A_EXT_B_INT, false); + checkPredicate(RelatePredicate.disjoint(), A_EXT_B_INT, true); + } + + public void testCovers() { + checkPredicate(RelatePredicate.covers(), A_INT_B_INT, true); + checkPredicate(RelatePredicate.covers(), A_EXT_B_INT, false); + } + + public void testCoversFast() { + checkPredicatePartial(RelatePredicate.covers(), A_EXT_B_INT, false); + } + + public void testMatch() { + checkPredicate(RelatePredicate.matches("1***T*0**"), "1**.*2*.0**", true); + } + + //======================================================= + + private void checkPredicate(TopologyPredicate pred, String im, boolean expected) { + applyIM(im, pred); + checkPred(pred, expected); + } + + private void checkPredicatePartial(TopologyPredicate pred, String im, boolean expected) { + applyIM(im, pred); + boolean isKnown = pred.isKnown(); + assertTrue("predicate value is not known", isKnown); + checkPred(pred, expected); + } + + private void checkPred(TopologyPredicate pred, boolean expected) { + pred.finish(); + boolean actual = pred.value(); + assertEquals(expected, actual); + } + + private static void applyIM(String imIn, TopologyPredicate pred) { + String im = cleanIM(imIn); + for (int i = 0; i < 9; i++) { + int locA = i / 3; + int locB = i - 3 * locA; + char entry = im.charAt(i); + if (entry == '0' || entry == '1' || entry == '2') { + int dim = Dimension.toDimensionValue(entry); + pred.updateDimension(locA, locB, dim); + } + } + } + + private static String cleanIM(String im) { + String im1 = im.replaceAll("\\.", ""); + return im1; + } + +} From fae342df1fe091bc658aa676229fcc7c4b348c0d Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 3 Jun 2024 21:33:58 -0700 Subject: [PATCH 090/123] Add copyright header --- .../jts/operation/relateng/RelatePredicateTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelatePredicateTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelatePredicateTest.java index f56f3c3544..f1acc94e7d 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelatePredicateTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelatePredicateTest.java @@ -1,3 +1,14 @@ +/* + * Copyright (c) 2024 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ package org.locationtech.jts.operation.relateng; import org.locationtech.jts.geom.Dimension; From c2a52fe1a0a9a9c53b986ae8850dffbdec1958b5 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 3 Jun 2024 21:34:14 -0700 Subject: [PATCH 091/123] Add LinearBoundary unit test --- .../operation/relateng/LinearBoundary.java | 12 +-- .../relateng/LinearBoundaryTest.java | 97 +++++++++++++++++++ 2 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 modules/core/src/test/java/org/locationtech/jts/operation/relateng/LinearBoundaryTest.java diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/LinearBoundary.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/LinearBoundary.java index 8267ae1394..1172a95a30 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/LinearBoundary.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/LinearBoundary.java @@ -49,6 +49,10 @@ private boolean checkBoundary(Map vertexDegree) { return false; } + public boolean hasBoundary() { + return hasBoundary; + } + public boolean isBoundary(Coordinate pt) { if (! vertexDegree.containsKey(pt)) return false; @@ -75,13 +79,5 @@ private static void addEndpoint(Coordinate p, Map degree) { dim++; degree.put(p, dim); } - - public Set getEndPoints() { - return vertexDegree.keySet(); - } - - public boolean hasBoundary() { - return hasBoundary; - } } diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/LinearBoundaryTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/LinearBoundaryTest.java new file mode 100644 index 0000000000..522f785700 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/LinearBoundaryTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2024 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.locationtech.jts.algorithm.BoundaryNodeRule; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.util.LineStringExtracter; + +import junit.textui.TestRunner; +import test.jts.GeometryTestCase; + +public class LinearBoundaryTest extends GeometryTestCase { + public static void main(String args[]) { + TestRunner.run(LinearBoundaryTest.class); + } + + public LinearBoundaryTest(String name) { + super(name); + } + + public void testLineMod2() { + checkLinearBoundary("LINESTRING (0 0, 9 9)", + BoundaryNodeRule.MOD2_BOUNDARY_RULE, + "MULTIPOINT((0 0), (9 9))"); + } + + public void testLines2Mod2() { + checkLinearBoundary("MULTILINESTRING ((0 0, 9 9), (9 9, 5 1))", + BoundaryNodeRule.MOD2_BOUNDARY_RULE, + "MULTIPOINT((0 0), (5 1))"); + } + + public void testLines3Mod2() { + checkLinearBoundary("MULTILINESTRING ((0 0, 9 9), (9 9, 5 1), (9 9, 1 5))", + BoundaryNodeRule.MOD2_BOUNDARY_RULE, + "MULTIPOINT((0 0), (5 1), (1 5), (9 9))"); + } + + public void testLines3Monvalent() { + checkLinearBoundary("MULTILINESTRING ((0 0, 9 9), (9 9, 5 1), (9 9, 1 5))", + BoundaryNodeRule.MONOVALENT_ENDPOINT_BOUNDARY_RULE, + "MULTIPOINT((0 0), (5 1), (1 5))"); + } + + private void checkLinearBoundary(String wkt, BoundaryNodeRule bnr, String wktBdyExpected) { + Geometry geom = read(wkt); + LinearBoundary lb = new LinearBoundary(extractLines(geom), bnr); + boolean hasBoundaryExpected = wktBdyExpected == null ? false : true; + assertEquals("HasBoundary", hasBoundaryExpected, lb.hasBoundary()); + + checkBoundaryPoints(lb, geom, wktBdyExpected); + } + + private void checkBoundaryPoints(LinearBoundary lb, Geometry geom, String wktBdyExpected) { + Set bdySet = extractPoints(wktBdyExpected); + + for (Coordinate p : bdySet) { + assertTrue(lb.isBoundary(p)); + } + + Coordinate[] allPts = geom.getCoordinates(); + for (Coordinate p : allPts) { + if (! bdySet.contains(p)) { + assertFalse(lb.isBoundary(p)); + } + } + } + + private Set extractPoints(String wkt) { + Set ptSet = new HashSet(); + if (wkt == null) return ptSet; + Coordinate[] pts = read(wkt).getCoordinates(); + for (Coordinate p : pts) { + ptSet.add(p); + } + return ptSet; + } + + private List extractLines(Geometry geom) { + return LineStringExtracter.getLines(geom); + } +} From d45d37ea2f98f307a561bb0bb787c61e6f0362b9 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 4 Jun 2024 16:50:39 -0700 Subject: [PATCH 092/123] Add RelateGeometry unit test --- .../relateng/RelateGeometryTest.java | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateGeometryTest.java diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateGeometryTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateGeometryTest.java new file mode 100644 index 0000000000..f5c511fe09 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateGeometryTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import java.util.Set; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; + +import junit.textui.TestRunner; +import test.jts.GeometryTestCase; + +public class RelateGeometryTest extends GeometryTestCase { + + public static void main(String args[]) { + TestRunner.run(RelateGeometryTest.class); + } + + public RelateGeometryTest(String name) { + super(name); + } + + public void testUniquePoints() { + Geometry geom = read("MULTIPOINT ((0 0), (5 5), (5 0), (0 0))"); + RelateGeometry rgeom = new RelateGeometry(geom); + Set pts = rgeom.getUniquePoints(); + assertEquals("Unique pts size", 3, pts.size()); + } + + public void testBoundary() { + Geometry geom = read("MULTILINESTRING ((0 0, 9 9), (9 9, 5 1))"); + RelateGeometry rgeom = new RelateGeometry(geom); + assertTrue("hasBoundary", rgeom.hasBoundary()); + } + + public void testHasDimension() { + Geometry geom = read("GEOMETRYCOLLECTION (POLYGON ((1 9, 5 9, 5 5, 1 5, 1 9)), LINESTRING (1 1, 5 4), POINT (6 5))"); + RelateGeometry rgeom = new RelateGeometry(geom); + assertTrue("hasDimension 0", rgeom.hasDimension(0)); + assertTrue("hasDimension 1", rgeom.hasDimension(1)); + assertTrue("hasDimension 2", rgeom.hasDimension(2)); + } + + public void testDimension() { + checkDimension("POINT (0 0)", 0, 0); + checkDimension("LINESTRING (0 0, 0 0)", 1, 0); + checkDimension("LINESTRING (0 0, 9 9)", 1, 1); + checkDimension("POLYGON ((1 9, 5 9, 5 5, 1 5, 1 9))", 2, 2); + checkDimension("GEOMETRYCOLLECTION (POLYGON ((1 9, 5 9, 5 5, 1 5, 1 9)), LINESTRING (1 1, 5 4), POINT (6 5))", 2, 2); + checkDimension("GEOMETRYCOLLECTION (POLYGON EMPTY, LINESTRING (1 1, 5 4), POINT (6 5))", 2, 1); + } + + private void checkDimension(String wkt, int expectedDim, int expectedDimReal) { + Geometry geom = read(wkt); + RelateGeometry rgeom = new RelateGeometry(geom); + assertEquals(expectedDim, rgeom.getDimension()); + assertEquals(expectedDimReal, rgeom.getDimensionReal()); + } + +} From f00cf3d5e63601b333d156fbe5f8136b44618d3e Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 4 Jun 2024 22:05:01 -0700 Subject: [PATCH 093/123] Simplify TopologyComputer logic --- .../operation/relateng/TopologyComputer.java | 95 +++++++------------ 1 file changed, 34 insertions(+), 61 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyComputer.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyComputer.java index de27064c94..b9e12fb927 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyComputer.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyComputer.java @@ -180,72 +180,38 @@ private NodeSections getNodeSections(Coordinate nodePt) { public void addIntersection(NodeSection a, NodeSection b) { if (! a.isSameGeometry(b)) { - updateABIntersection(a, b); + updateIntersectionAB(a, b); } - //TODO: for self-intersections add virtual nodes to geometry components - - //-- add edges to node to allow full topology update later + //-- add edges to node to allow full topology evaluation later addNodeSections(a, b); } - - private void addNodeSections(NodeSection ns0, NodeSection ns1) { - NodeSections sections = getNodeSections(ns0.nodePt()); - sections.addNodeSection(ns0); - sections.addNodeSection(ns1); - } - - private void updateABIntersection(NodeSection a, NodeSection b) { - if (NodeSection.isProper(a, b)) { - updateABIntersectionProper(a, b); - } - else if (NodeSection.isAreaArea(a, b)) { - updateAreaAreaCross(a, b); - } - updateNodeLocation(a.nodePt(), a, b); - } - private void updateABIntersectionProper(NodeSection a, NodeSection b) { - int dimA = a.dimension(); - int dimB = b.dimension(); - if (dimA == 2 && dimB == 2) { - //- a proper edge intersection is an edge cross. + /** + * Update topology for an intersection between A and B. + * + * @param a the section for geometry A + * @param b the section for geometry B + */ + private void updateIntersectionAB(NodeSection a, NodeSection b) { + if (NodeSection.isAreaArea(a, b)) { updateAreaAreaCross(a, b); } - else if (dimA == 2 && dimB == 1) { - updateAreaLineCross(a, b); - } - else if (dimA == 1 && dimB == 2) { - updateAreaLineCross(b, a); - } - else if (dimA == 1 && dimB == 1) { - //-- nothing to do here - node topology is updated by caller - } - else { - Assert.shouldNeverReachHere(MSG_GEOMETRY_DIMENSION_UNEXPECTED); - } - } - - private void updateAreaLineCross(NodeSection eArea, NodeSection eLine) { - //TODO: does this give any info apart from node? which is checked by caller - /** - * A proper crossing of a line and and area - * provides limited topological information, - * since the area edge intersection point - * may also be a node of a hole, or of another shell, or both. - * Full topology is determined when the node topology is evaluated. - */ - boolean geomLine = eLine.isA(); - Coordinate nodePt = eArea.nodePt(); - int locLine = getGeometry(geomLine).locateNode(nodePt, eLine.getPolygonal()); - int locArea = getGeometry(eArea.isA()).locateNode(nodePt, eArea.getPolygonal()); - updateDim(eArea.isA(), locArea, locLine, Dimension.P); + updateNodeLocation(a, b); } + /** + * Updates topology for an AB Area-Area crossing node. + * Sections cross at a node if (a) the intersection is proper + * (i.e. in the interior of two segments) + * or (b) if non-proper then whether the linework crosses + * is determined by the geometry of the segments on either side of the node. + * In these situations the area geometry interiors intersect (in dimension 2). + * + * @param a the section for geometry A + * @param b the section for geometry B + */ private void updateAreaAreaCross(NodeSection a, NodeSection b) { boolean isProper = NodeSection.isProper(a, b); - /** - * A crossing of area edges determines that the interiors intersect. - */ if (isProper || PolygonNodeTopology.isCrossing(a.nodePt(), a.getVertex(0), a.getVertex(1), b.getVertex(0), b.getVertex(1))) { @@ -253,17 +219,24 @@ private void updateAreaAreaCross(NodeSection a, NodeSection b) { } } /** - * Adds a basic edge intersection point. - * @param pt - * @param b - * @param a + * Updates topology for a node at an AB edge intersection. + * + * @param a the section for geometry A + * @param b the section for geometry B */ - private void updateNodeLocation(Coordinate pt, NodeSection a, NodeSection b) { + private void updateNodeLocation(NodeSection a, NodeSection b) { + Coordinate pt = a.nodePt(); int locA = geomA.locateNode(pt, a.getPolygonal()); int locB = geomB.locateNode(pt, b.getPolygonal()); updateDim(locA, locB, Dimension.P); } + private void addNodeSections(NodeSection ns0, NodeSection ns1) { + NodeSections sections = getNodeSections(ns0.nodePt()); + sections.addNodeSection(ns0); + sections.addNodeSection(ns1); + } + public void addPointOnPointInterior(Coordinate pt) { updateDim(Location.INTERIOR, Location.INTERIOR, Dimension.P); } From 24c92207cfe3e4175949a66a07fd50016f87d75c Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 4 Jun 2024 22:05:32 -0700 Subject: [PATCH 094/123] Javadoc --- .../jts/operation/relateng/DimensionLocation.java | 7 +++++++ .../jts/operation/relateng/NodeSection.java | 6 ++++++ .../jts/operation/relateng/RelateGeometry.java | 10 ++++++++++ .../jts/operation/relateng/RelatePointLocator.java | 10 +++++++++- 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/DimensionLocation.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/DimensionLocation.java index 6aa32b286d..bcdc421ac2 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/DimensionLocation.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/DimensionLocation.java @@ -14,6 +14,13 @@ import org.locationtech.jts.geom.Dimension; import org.locationtech.jts.geom.Location; +/** + * Codes which combine a geometry dimension and a location + * on the geometry. + * + * @author mdavis + * + */ class DimensionLocation { public static final int EXTERIOR = Location.EXTERIOR; diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/NodeSection.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/NodeSection.java index dd59d7bf50..f707df0109 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/NodeSection.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/NodeSection.java @@ -95,6 +95,12 @@ public int ringId() { return ringId; } + /** + * Gets the polygon this section is part of. + * Will be null if section is not on a polygon boundary. + * + * @return the associated polygon, or null + */ public Geometry getPolygonal() { return poly; } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateGeometry.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateGeometry.java index 57ad0ef5ae..e84713ad63 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateGeometry.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateGeometry.java @@ -178,6 +178,12 @@ public boolean hasDimension(int dim) { return false; } + /** + * Gets the actual non-empty dimension of the geometry. + * Zero-length LineStrings are treated as Points. + * + * @return the real (non-empty) dimension + */ public int getDimensionReal() { if (isGeomEmpty) return Dimension.FALSE; if (getDimension() == 1 && isLineZeroLen) @@ -208,6 +214,10 @@ public int locateLineEnd(Coordinate p) { /** * Locates a vertex of a polygon. + * A vertex of a Polygon or MultiPolygon is on + * the {@link Location#BOUNDARY}. + * But a vertex of an overlapped polygon in a GeometryCollection + * may be in the {@link Location#INTERIOR}. * * @param pt the polygon vertex * @return the location of the vertex diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePointLocator.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePointLocator.java index 35ae78f2e9..f4942e78c3 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePointLocator.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePointLocator.java @@ -152,13 +152,21 @@ public int locateLineEnd(Coordinate p) { * (i.e. a point or on an edge). * * @param p the node point to locate - * @param polygonal + * @param parentPolygonal the polygon the point is a node of * @return the location of the node point */ public int locateNode(Coordinate p, Geometry parentPolygonal) { return DimensionLocation.location(locateNodeWithDim(p, parentPolygonal)); } + /** + * Locates a point which is known to be a node of the geometry, + * as a {@link DimensionLocation}. + * + * @param p the point to locate + * @param parentPolygonal the polygon the point is a node of + * @return the dimension and location of the point + */ public int locateNodeWithDim(Coordinate p, Geometry parentPolygonal) { return locateWithDim(p, true, parentPolygonal); } From 1672c0386bdd4f1c06935defcd50a3d95e7f414c Mon Sep 17 00:00:00 2001 From: James Willis Date: Mon, 10 Jun 2024 13:45:15 -0700 Subject: [PATCH 095/123] Modify GeometryFixer doc to reflect that a fixed polygon may become a multipolygon (#1058) Signed-off-by: jameswillis Co-authored-by: jameswillis --- .../main/java/org/locationtech/jts/geom/util/GeometryFixer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/geom/util/GeometryFixer.java b/modules/core/src/main/java/org/locationtech/jts/geom/util/GeometryFixer.java index 7e1c2c0d9d..c8305a21c7 100644 --- a/modules/core/src/main/java/org/locationtech/jts/geom/util/GeometryFixer.java +++ b/modules/core/src/main/java/org/locationtech/jts/geom/util/GeometryFixer.java @@ -50,7 +50,7 @@ *
  • Point: keep valid coordinate, or EMPTY
  • *
  • LineString: coordinates are fixed
  • *
  • LinearRing: coordinates are fixed. Keep valid ring, or else convert into LineString
  • - *
  • Polygon: transform into a valid polygon, + *
  • Polygon: transform into a valid polygon or multipolygon, * preserving as much of the extent and vertices as possible. * + * See also {@link #setOutputOrdinates(EnumSet)} * * @param outputDimension the coordinate dimension to output (2 to 4) */ diff --git a/modules/core/src/test/java/org/locationtech/jts/io/WKBTest.java b/modules/core/src/test/java/org/locationtech/jts/io/WKBTest.java index 6928f4184a..95025daca7 100644 --- a/modules/core/src/test/java/org/locationtech/jts/io/WKBTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/io/WKBTest.java @@ -12,6 +12,7 @@ package org.locationtech.jts.io; import java.io.IOException; +import java.util.EnumSet; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateFilter; @@ -152,6 +153,49 @@ public void testGeometryCollectionEmpty() runWKBTest("GEOMETRYCOLLECTION EMPTY"); } + /** + * Tests if a previously written WKB with M-coordinates can be read as expected. + */ + public void testWriteAndReadM() throws ParseException + { + String wkt = "MULTILINESTRING M((1 1 1, 2 2 2))"; + WKTReader wktReader = new WKTReader(); + Geometry geometryBefore = wktReader.read(wkt); + + WKBWriter wkbWriter = new WKBWriter(3); + wkbWriter.setOutputOrdinates(EnumSet.of(Ordinate.X, Ordinate.Y, Ordinate.M)); + byte[] write = wkbWriter.write(geometryBefore); + + WKBReader wkbReader = new WKBReader(); + Geometry geometryAfter = wkbReader.read(write); + + assertEquals(1.0, geometryAfter.getCoordinates()[0].getX()); + assertEquals(1.0, geometryAfter.getCoordinates()[0].getY()); + assertEquals(Double.NaN, geometryAfter.getCoordinates()[0].getZ()); + assertEquals(1.0, geometryAfter.getCoordinates()[0].getM()); + } + + /** + * Tests if a previously written WKB with Z-coordinates can be read as expected. + */ + public void testWriteAndReadZ() throws ParseException + { + String wkt = "MULTILINESTRING ((1 1 1, 2 2 2))"; + WKTReader wktReader = new WKTReader(); + Geometry geometryBefore = wktReader.read(wkt); + + WKBWriter wkbWriter = new WKBWriter(3); + byte[] write = wkbWriter.write(geometryBefore); + + WKBReader wkbReader = new WKBReader(); + Geometry geometryAfter = wkbReader.read(write); + + assertEquals(1.0, geometryAfter.getCoordinates()[0].getX()); + assertEquals(1.0, geometryAfter.getCoordinates()[0].getY()); + assertEquals(1.0, geometryAfter.getCoordinates()[0].getZ()); + assertEquals(Double.NaN, geometryAfter.getCoordinates()[0].getM()); + } + private void runWKBTest(String wkt) throws IOException, ParseException { runWKBTestCoordinateArray(wkt); diff --git a/modules/core/src/test/java/org/locationtech/jts/io/WKBWriterTest.java b/modules/core/src/test/java/org/locationtech/jts/io/WKBWriterTest.java index 683cb7dace..d70276c5b5 100644 --- a/modules/core/src/test/java/org/locationtech/jts/io/WKBWriterTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/io/WKBWriterTest.java @@ -12,8 +12,10 @@ package org.locationtech.jts.io; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateXYZM; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.Point; import junit.textui.TestRunner; @@ -125,6 +127,25 @@ public void testGeometryCollection() { 4326, "0107000020E61000000900000001010000000000000000000000000000000000F03F01010000000000000000000000000000000000F03F01010000000000000000000040000000000000084001020000000200000000000000000000400000000000000840000000000000104000000000000014400102000000020000000000000000000000000000000000F03F000000000000004000000000000008400102000000020000000000000000001040000000000000144000000000000018400000000000001C4001030000000200000005000000000000000000000000000000000000000000000000000000000000000000244000000000000024400000000000002440000000000000244000000000000000000000000000000000000000000000000005000000000000000000F03F000000000000F03F000000000000F03F0000000000002240000000000000224000000000000022400000000000002240000000000000F03F000000000000F03F000000000000F03F01030000000200000005000000000000000000000000000000000000000000000000000000000000000000244000000000000024400000000000002440000000000000244000000000000000000000000000000000000000000000000005000000000000000000F03F000000000000F03F000000000000F03F0000000000002240000000000000224000000000000022400000000000002240000000000000F03F000000000000F03F000000000000F03F0103000000010000000500000000000000000022C0000000000000000000000000000022C00000000000002440000000000000F0BF0000000000002440000000000000F0BF000000000000000000000000000022C00000000000000000"); } + + public void testWkbLineStringZM() throws ParseException { + LineString lineZM = new GeometryFactory().createLineString(new Coordinate[]{new CoordinateXYZM(1,2,3,4), new CoordinateXYZM(5,6,7,8)}); + byte[] write = new WKBWriter(4).write(lineZM); + + LineString deserialisiert = (LineString) new WKBReader().read(write); + + assertEquals(lineZM, deserialisiert); + + assertEquals(1.0, lineZM.getPointN(0).getCoordinate().getX()); + assertEquals(2.0, lineZM.getPointN(0).getCoordinate().getY()); + assertEquals(3.0, lineZM.getPointN(0).getCoordinate().getZ()); + assertEquals(4.0, lineZM.getPointN(0).getCoordinate().getM()); + + assertEquals(5.0, lineZM.getPointN(1).getCoordinate().getX()); + assertEquals(6.0, lineZM.getPointN(1).getCoordinate().getY()); + assertEquals(7.0, lineZM.getPointN(1).getCoordinate().getZ()); + assertEquals(8.0, lineZM.getPointN(1).getCoordinate().getM()); + } void checkWKB(String wkt, int dimension, String expectedWKBHex) { checkWKB(wkt, dimension, ByteOrderValues.LITTLE_ENDIAN, -1, expectedWKBHex); diff --git a/modules/core/src/test/java/org/locationtech/jts/io/WKTWriterTest.java b/modules/core/src/test/java/org/locationtech/jts/io/WKTWriterTest.java index 66e573e390..f2d4eb4577 100644 --- a/modules/core/src/test/java/org/locationtech/jts/io/WKTWriterTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/io/WKTWriterTest.java @@ -181,4 +181,22 @@ public void testWrite3D_withNaN() { assertEquals("LINESTRING (1 1, 2 2)", wkt); } + public void testWktLineStringZM() throws ParseException { + LineString lineZM = new GeometryFactory().createLineString(new Coordinate[]{new CoordinateXYZM(1,2,3,4), new CoordinateXYZM(5,6,7,8)}); + String write = new WKTWriter(4).write(lineZM); + + LineString deserialisiert = (LineString) new WKTReader().read(write); + + assertEquals(lineZM, deserialisiert); + + assertEquals(1.0, lineZM.getPointN(0).getCoordinate().getX()); + assertEquals(2.0, lineZM.getPointN(0).getCoordinate().getY()); + assertEquals(3.0, lineZM.getPointN(0).getCoordinate().getZ()); + assertEquals(4.0, lineZM.getPointN(0).getCoordinate().getM()); + + assertEquals(5.0, lineZM.getPointN(1).getCoordinate().getX()); + assertEquals(6.0, lineZM.getPointN(1).getCoordinate().getY()); + assertEquals(7.0, lineZM.getPointN(1).getCoordinate().getZ()); + assertEquals(8.0, lineZM.getPointN(1).getCoordinate().getM()); + } } From 02e16242ac5b6a5c4d2bd0787a22fb11973084b2 Mon Sep 17 00:00:00 2001 From: Jody Garnett Date: Mon, 26 Aug 2024 10:59:42 -0700 Subject: [PATCH 118/123] Release notes for 1.2.0.0 release mid September (#1070) --- doc/JTS_Version_History.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 928f495a76..cd6ebba73c 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -17,9 +17,23 @@ Distributions for older JTS versions can be obtained at the +Version 1.x + +Release Date: TBD + # Version 1.x -*Release Date: TBD* +### New Features + +### Functionality Improvements + +### Bug Fixes + +### Performance Improvements + +# Version 1.20.0 + +*Release Date: 09/18/2024* ### New Features * Add `CoverageValidator` `CoveragePolygonValidator` (#900) @@ -44,6 +58,7 @@ Distributions for older JTS versions can be obtained at the * Improve CoverageSimplifier with ring removal, smoothing, inner/outer and per-feature tolerances (#1060) ### Bug Fixes +* Fix `WKBReader` and `WKBWriter` handling of M measures when writing to WKB and reading from WKB (#734) * Fix `PreparedGeometry` handling of EMPTY elements (#904) * Fix `WKBReader` parsing of WKB containing multiple empty elements (#905) * Fix `LineSegment.orientationIndex(LineSegment)` to correct orientation for non-collinear segments on right (#914) From dc7078b015947aae9ca15978e828c02978250f4c Mon Sep 17 00:00:00 2001 From: Jody Garnett Date: Mon, 26 Aug 2024 15:17:07 -0700 Subject: [PATCH 119/123] remove travis build badge and fix some links --- .travis.yml | 10 ---------- README.md | 9 ++++----- 2 files changed, 4 insertions(+), 15 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6d037e3607..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -language: java -jdk: - - oraclejdk8 -sudo: false -cache: - directories: - - $HOME/.m2 -install: mvn install -B -V -dist: trusty - diff --git a/README.md b/README.md index 2e897c4bc3..4cc97937dd 100644 --- a/README.md +++ b/README.md @@ -5,18 +5,17 @@ The JTS Topology Suite is a Java library for creating and manipulating vector ge ![JTS logo](jts_logo.png) -[![Travis Build Status](https://api.travis-ci.org/locationtech/jts.svg)](http://travis-ci.org/locationtech/jts) [![GitHub Action Status](https://github.com/locationtech/jts/workflows/GitHub%20CI/badge.svg)](https://github.com/locationtech/jts/actions) +[![GitHub Action Status](https://github.com/locationtech/jts/workflows/GitHub%20CI/badge.svg)](https://github.com/locationtech/jts/actions) [![Join the chat at https://gitter.im/locationtech/jts](https://badges.gitter.im/locationtech/jts.svg)](https://gitter.im/locationtech/jts?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - -JTS is a project in the [LocationTech](http://www.locationtech.org) working group of the Eclipse Foundation. +JTS is a project in the [LocationTech](https://www.locationtech.org) working group of the Eclipse Foundation. ![LocationTech](locationtech_mark.png) ## Requirements -Currently JTS targets Java 1.8 and above. +Currently JTS targets Java 8 and above. ## Resources @@ -41,7 +40,7 @@ Currently JTS targets Java 1.8 and above. JTS is open source software. It is dual-licensed under: * [Eclipse Public License 2.0](https://www.eclipse.org/legal/epl-v20.html) -* [Eclipse Distribution License 1.0](http://www.eclipse.org/org/documents/edl-v10.php) (a BSD Style License) +* [Eclipse Distribution License 1.0](https://www.eclipse.org/org/documents/edl-v10.php) (a BSD Style License) See also: From b78fe0145588954bb8408a1a230580f0d68e2a5a Mon Sep 17 00:00:00 2001 From: Jody Garnett Date: Mon, 26 Aug 2024 15:17:23 -0700 Subject: [PATCH 120/123] Update user guide maven repo information --- USING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/USING.md b/USING.md index 0ce1b50cef..0dae801426 100644 --- a/USING.md +++ b/USING.md @@ -44,7 +44,7 @@ Our [build server](https://ci.eclipse.org/jts/) publishes to the LocationTech Ma locationtech-releases - https://repo.eclipse.org/content/groups/releases + https://repo.eclipse.org/content/repositories/jts false @@ -66,7 +66,7 @@ The latest snapshot builds are now avaialble: ```xml - 1.17.0-SNAPSHOT + 1.20.0-SNAPSHOT org.locationtech.jts From d21902f07af73c7adbae26a0d9c2929af0152904 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 27 Aug 2024 12:24:53 -0700 Subject: [PATCH 121/123] Add flag to enable RelateNG (#1073) --- USING.md | 1 + .../org/locationtech/jts/geom/Geometry.java | 74 +----- .../locationtech/jts/geom/GeometryRelate.java | 217 ++++++++++++++++++ 3 files changed, 230 insertions(+), 62 deletions(-) create mode 100644 modules/core/src/main/java/org/locationtech/jts/geom/GeometryRelate.java diff --git a/USING.md b/USING.md index 0dae801426..6e2dbdf677 100644 --- a/USING.md +++ b/USING.md @@ -123,6 +123,7 @@ module org.foo.baz { ## JTS System Properties * `-Djts.overlay=ng` enables the use of OverlayNG in `Geometry` overlay methods. (*Note: in a future release this will become the default behaviour*) +* `-Djts.relate=ng` enables the use of RelateNG in `Geometry` topological predicate methods. (*Note: in a future release this will become the default behaviour*) ## JTS Tools diff --git a/modules/core/src/main/java/org/locationtech/jts/geom/Geometry.java b/modules/core/src/main/java/org/locationtech/jts/geom/Geometry.java index ba0d353fb2..104a885c8e 100644 --- a/modules/core/src/main/java/org/locationtech/jts/geom/Geometry.java +++ b/modules/core/src/main/java/org/locationtech/jts/geom/Geometry.java @@ -713,10 +713,7 @@ public boolean disjoint(Geometry g) { * Returns false if both Geometrys are points */ public boolean touches(Geometry g) { - // short-circuit test - if (! getEnvelopeInternal().intersects(g.getEnvelopeInternal())) - return false; - return relate(g).isTouches(getDimension(), g.getDimension()); + return GeometryRelate.touches(this, g); } /** @@ -771,18 +768,8 @@ public boolean intersects(Geometry g) { if (g.isRectangle()) { return RectangleIntersects.intersects((Polygon) g, this); } - if (isGeometryCollection() || g.isGeometryCollection()) { - for (int i = 0 ; i < getNumGeometries() ; i++) { - for (int j = 0 ; j < g.getNumGeometries() ; j++) { - if (getGeometryN(i).intersects(g.getGeometryN(j))) { - return true; - } - } - } - return false; - } - // general case - return relate(g).isIntersects(); + + return GeometryRelate.intersects(this, g); } /** @@ -845,7 +832,7 @@ public boolean crosses(Geometry g) { * @see Geometry#coveredBy */ public boolean within(Geometry g) { - return g.contains(this); + return GeometryRelate.within(this, g); } /** @@ -876,25 +863,13 @@ public boolean within(Geometry g) { * @see Geometry#covers */ public boolean contains(Geometry g) { - // optimization - lower dimension cannot contain areas - if (g.getDimension() == 2 && getDimension() < 2) { - return false; - } - // optimization - P cannot contain a non-zero-length L - // Note that a point can contain a zero-length lineal geometry, - // since the line has no boundary due to Mod-2 Boundary Rule - if (g.getDimension() == 1 && getDimension() < 1 && g.getLength() > 0.0) { - return false; - } - // optimization - envelope test - if (! getEnvelopeInternal().contains(g.getEnvelopeInternal())) - return false; + // optimization for rectangle arguments if (isRectangle()) { return RectangleContains.contains((Polygon) this, g); } // general case - return relate(g).isContains(); + return GeometryRelate.contains(this, g); } /** @@ -919,10 +894,7 @@ public boolean contains(Geometry g) { *@return true if the two Geometrys overlap. */ public boolean overlaps(Geometry g) { - // short-circuit test - if (! getEnvelopeInternal().intersects(g.getEnvelopeInternal())) - return false; - return relate(g).isOverlaps(getDimension(), g.getDimension()); + return GeometryRelate.overlaps(this, g); } /** @@ -960,24 +932,7 @@ public boolean overlaps(Geometry g) { * @see Geometry#coveredBy */ public boolean covers(Geometry g) { - // optimization - lower dimension cannot cover areas - if (g.getDimension() == 2 && getDimension() < 2) { - return false; - } - // optimization - P cannot cover a non-zero-length L - // Note that a point can cover a zero-length lineal geometry - if (g.getDimension() == 1 && getDimension() < 1 && g.getLength() > 0.0) { - return false; - } - // optimization - envelope test - if (! getEnvelopeInternal().covers(g.getEnvelopeInternal())) - return false; - // optimization for rectangle arguments - if (isRectangle()) { - // since we have already tested that the test envelope is covered - return true; - } - return relate(g).isCovers(); + return GeometryRelate.covers(this, g); } /** @@ -1010,7 +965,7 @@ public boolean covers(Geometry g) { * @see Geometry#covers */ public boolean coveredBy(Geometry g) { - return g.covers(this); + return GeometryRelate.coveredBy(this, g); } /** @@ -1037,7 +992,7 @@ public boolean coveredBy(Geometry g) { * @see IntersectionMatrix */ public boolean relate(Geometry g, String intersectionPattern) { - return relate(g).matches(intersectionPattern); + return GeometryRelate.relate(this, g, intersectionPattern); } /** @@ -1048,9 +1003,7 @@ public boolean relate(Geometry g, String intersectionPattern) { * boundaries and exteriors of the two Geometrys */ public IntersectionMatrix relate(Geometry g) { - checkNotGeometryCollection(this); - checkNotGeometryCollection(g); - return RelateOp.relate(this, g); + return GeometryRelate.relate(this, g); } /** @@ -1101,10 +1054,7 @@ public boolean equals(Geometry g) { */ public boolean equalsTopo(Geometry g) { - // short-circuit test - if (! getEnvelopeInternal().equals(g.getEnvelopeInternal())) - return false; - return relate(g).isEquals(getDimension(), g.getDimension()); + return GeometryRelate.equalsTopo(this, g); } /** diff --git a/modules/core/src/main/java/org/locationtech/jts/geom/GeometryRelate.java b/modules/core/src/main/java/org/locationtech/jts/geom/GeometryRelate.java new file mode 100644 index 0000000000..615e7d1ab8 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/geom/GeometryRelate.java @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2020 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.geom; + +import org.locationtech.jts.operation.relate.RelateOp; +import org.locationtech.jts.operation.relateng.RelateNG; +import org.locationtech.jts.operation.relateng.RelatePredicate; + +/** + * Internal class which encapsulates the runtime switch to use RelateNG. + *

    + * This class allows the {@link Geometry} predicate methods to be + * switched between the original {@link RelateOp} algorithm + * and the modern {@link RelateNG} codebase + * via a system property jts.relate. + *

      + *
    • jts.relate=old - (default) use original RelateOp algorithm + *
    • jts.relate=ng - use RelateNG + *
    + * + * @author mdavis + * + */ +class GeometryRelate +{ + public static String RELATE_PROPERTY_NAME = "jts.relate"; + + public static String RELATE_PROPERTY_VALUE_NG = "ng"; + public static String RELATE_PROPERTY_VALUE_OLD = "old"; + + /** + * Currently the old relate implementation is the default + */ + public static boolean RELATE_NG_DEFAULT = false; + + private static boolean isRelateNG = RELATE_NG_DEFAULT; + + static { + setRelateImpl(System.getProperty(RELATE_PROPERTY_NAME)); + } + + /** + * This function is provided primarily for unit testing. + * It is not recommended to use it dynamically, since + * that may result in inconsistent overlay behaviour. + * + * @param relateImplCode the code for the overlay method (may be null) + */ + static void setRelateImpl(String relateImplCode) { + if (relateImplCode == null) + return; + // set flag explicitly since current value may not be default + isRelateNG = RELATE_NG_DEFAULT; + + if (RELATE_PROPERTY_VALUE_NG.equalsIgnoreCase(relateImplCode) ) + isRelateNG = true; + } + + static boolean intersects(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b, RelatePredicate.intersects()); + } + if (a.isGeometryCollection() || b.isGeometryCollection()) { + for (int i = 0 ; i < a.getNumGeometries() ; i++) { + for (int j = 0 ; j < b.getNumGeometries() ; j++) { + if (a.getGeometryN(i).intersects(b.getGeometryN(j))) { + return true; + } + } + } + return false; + } + return RelateOp.relate(a, b).isIntersects(); + } + + static boolean contains(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b, RelatePredicate.contains()); + } + // optimization - lower dimension cannot contain areas + if (b.getDimension() == 2 && a.getDimension() < 2) { + return false; + } + // optimization - P cannot contain a non-zero-length L + // Note that a point can contain a zero-length lineal geometry, + // since the line has no boundary due to Mod-2 Boundary Rule + if (b.getDimension() == 1 && a.getDimension() < 1 && b.getLength() > 0.0) { + return false; + } + // optimization - envelope test + if (! a.getEnvelopeInternal().contains(b.getEnvelopeInternal())) + return false; + return RelateOp.relate(a, b).isContains(); + } + + static boolean covers(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b, RelatePredicate.covers()); + } + // optimization - lower dimension cannot cover areas + if (b.getDimension() == 2 && a.getDimension() < 2) { + return false; + } + // optimization - P cannot cover a non-zero-length L + // Note that a point can cover a zero-length lineal geometry + if (b.getDimension() == 1 && a.getDimension() < 1 && b.getLength() > 0.0) { + return false; + } + // optimization - envelope test + if (! a.getEnvelopeInternal().covers(b.getEnvelopeInternal())) + return false; + // optimization for rectangle arguments + if (a.isRectangle()) { + // since we have already tested that the test envelope is covered + return true; + } + return RelateOp.relate(a, b).isCovers(); + } + + static boolean coveredBy(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b, RelatePredicate.coveredBy()); + } + return covers(b, a); + } + + static boolean crosses(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b, RelatePredicate.crosses()); + } + // short-circuit test + if (! a.getEnvelopeInternal().intersects(b.getEnvelopeInternal())) + return false; + return RelateOp.relate(a, b).isCrosses(a.getDimension(), b.getDimension()); + } + + static boolean disjoint(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b, RelatePredicate.disjoint()); + } + return ! intersects(a, b); + } + + static boolean equalsTopo(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b, RelatePredicate.equalsTopo()); + } + if (! a.getEnvelopeInternal().equals(b.getEnvelopeInternal())) + return false; + return RelateOp.relate(a, b).isEquals(a.getDimension(), b.getDimension()); + } + + static boolean overlaps(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b, RelatePredicate.overlaps()); + } + if (! a.getEnvelopeInternal().intersects(b.getEnvelopeInternal())) + return false; + return RelateOp.relate(a, b).isOverlaps(a.getDimension(), b.getDimension()); + } + + static boolean touches(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b, RelatePredicate.touches()); + } + if (! a.getEnvelopeInternal().intersects(b.getEnvelopeInternal())) + return false; + return RelateOp.relate(a, b).isTouches(a.getDimension(), b.getDimension()); + } + + static boolean within(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b, RelatePredicate.within()); + } + return contains(b, a); + } + + static IntersectionMatrix relate(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b); + } + Geometry.checkNotGeometryCollection(a); + Geometry.checkNotGeometryCollection(b); + return RelateOp.relate(a, b); + } + + static boolean relate(Geometry a, Geometry b, String intersectionPattern) + { + if (isRelateNG) { + return RelateNG.relate(a, b, intersectionPattern); + } + Geometry.checkNotGeometryCollection(a); + Geometry.checkNotGeometryCollection(b); + return RelateOp.relate(a, b).matches(intersectionPattern); + } + +} From f37193a312a6181678bec82782cb6f939edcff53 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 27 Aug 2024 12:29:32 -0700 Subject: [PATCH 122/123] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index cd6ebba73c..2276f09a3b 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -45,7 +45,8 @@ Release Date: TBD * Add `ConcaveHull.alphaShape` function (#952) * Add `OffsetCurve` Joined mode (#956) * Add `PointLocation.isOnSegment` function (#1048) -* Addd `RelateNG` API for improved topological relationship functionality and performance (#1052, #1055) +* Add `RelateNG` API for improved topological relationship functionality and performance (#1052, #1055) +* Add system property `jts.relate=ng` to enable use of RelateNG in `Geometry` methods (#1073) ### Functionality Improvements * Improve `TopologyPreservingSimplifier` to prevent edge-disjoint line collapse (#925) From 6e95fe82feb986a7aa657f4ffa406d8c290af509 Mon Sep 17 00:00:00 2001 From: Jody Garnett Date: Thu, 29 Aug 2024 22:31:21 -0700 Subject: [PATCH 123/123] Release version 1.20.0 --- build-tools/pom.xml | 2 +- modules/app/pom.xml | 2 +- modules/core/pom.xml | 2 +- modules/core/src/main/java/org/locationtech/jts/JTSVersion.java | 2 +- modules/example/pom.xml | 2 +- modules/io/common/pom.xml | 2 +- modules/io/ora/pom.xml | 2 +- modules/io/pom.xml | 2 +- modules/lab/pom.xml | 2 +- modules/pom.xml | 2 +- modules/tests/pom.xml | 2 +- pom.xml | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build-tools/pom.xml b/build-tools/pom.xml index 2dbf23033a..ba82456142 100644 --- a/build-tools/pom.xml +++ b/build-tools/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.locationtech.jts build-tools - 1.20.0-SNAPSHOT + 1.20.0 JTS Topology Suite Build Configuration diff --git a/modules/app/pom.xml b/modules/app/pom.xml index bea8faf0a9..e25145a9e9 100644 --- a/modules/app/pom.xml +++ b/modules/app/pom.xml @@ -3,7 +3,7 @@ org.locationtech.jts jts-modules - 1.20.0-SNAPSHOT + 1.20.0 jts-app ${project.groupId}:${project.artifactId} diff --git a/modules/core/pom.xml b/modules/core/pom.xml index a174d08ec1..4b5a72589b 100644 --- a/modules/core/pom.xml +++ b/modules/core/pom.xml @@ -3,7 +3,7 @@ org.locationtech.jts jts-modules - 1.20.0-SNAPSHOT + 1.20.0 jts-core ${project.groupId}:${project.artifactId} diff --git a/modules/core/src/main/java/org/locationtech/jts/JTSVersion.java b/modules/core/src/main/java/org/locationtech/jts/JTSVersion.java index 170b15d03e..45da12823f 100644 --- a/modules/core/src/main/java/org/locationtech/jts/JTSVersion.java +++ b/modules/core/src/main/java/org/locationtech/jts/JTSVersion.java @@ -46,7 +46,7 @@ public class JTSVersion { /** * An optional string providing further release info (such as "alpha 1"); */ - private static final String RELEASE_INFO = "SNAPSHOT"; + private static final String RELEASE_INFO = ""; /** * Prints the current JTS version to stdout. diff --git a/modules/example/pom.xml b/modules/example/pom.xml index cc4eadfe24..5dede0e79f 100644 --- a/modules/example/pom.xml +++ b/modules/example/pom.xml @@ -3,7 +3,7 @@ org.locationtech.jts jts-modules - 1.20.0-SNAPSHOT + 1.20.0 jts-example ${project.groupId}:${project.artifactId} diff --git a/modules/io/common/pom.xml b/modules/io/common/pom.xml index 7b2154cf5f..5ea29f37e7 100644 --- a/modules/io/common/pom.xml +++ b/modules/io/common/pom.xml @@ -3,7 +3,7 @@ org.locationtech.jts jts-io - 1.20.0-SNAPSHOT + 1.20.0 org.locationtech.jts.io jts-io-common diff --git a/modules/io/ora/pom.xml b/modules/io/ora/pom.xml index 9d7ef909e4..0c92dd48c7 100644 --- a/modules/io/ora/pom.xml +++ b/modules/io/ora/pom.xml @@ -4,7 +4,7 @@ org.locationtech.jts jts-io - 1.20.0-SNAPSHOT + 1.20.0 org.locationtech.jts.io jts-io-ora diff --git a/modules/io/pom.xml b/modules/io/pom.xml index 337127fe15..7b6cbb13ec 100644 --- a/modules/io/pom.xml +++ b/modules/io/pom.xml @@ -3,7 +3,7 @@ org.locationtech.jts jts-modules - 1.20.0-SNAPSHOT + 1.20.0 jts-io ${project.groupId}:${project.artifactId} diff --git a/modules/lab/pom.xml b/modules/lab/pom.xml index 6b44fd95c4..e5d1ae11ec 100644 --- a/modules/lab/pom.xml +++ b/modules/lab/pom.xml @@ -3,7 +3,7 @@ org.locationtech.jts jts-modules - 1.20.0-SNAPSHOT + 1.20.0 jts-lab ${project.groupId}:${project.artifactId} diff --git a/modules/pom.xml b/modules/pom.xml index d02e81a457..4a948e776f 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -3,7 +3,7 @@ org.locationtech.jts jts - 1.20.0-SNAPSHOT + 1.20.0 jts-modules ${project.groupId}:${project.artifactId} diff --git a/modules/tests/pom.xml b/modules/tests/pom.xml index 03b41f2940..15a33b70d5 100644 --- a/modules/tests/pom.xml +++ b/modules/tests/pom.xml @@ -3,7 +3,7 @@ org.locationtech.jts jts-modules - 1.20.0-SNAPSHOT + 1.20.0 jts-tests ${project.groupId}:${project.artifactId} diff --git a/pom.xml b/pom.xml index 7b91f97e3c..7589896f06 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.locationtech.jts jts - 1.20.0-SNAPSHOT + 1.20.0 pom JTS Topology Suite