From 75c4d1391b2db0cdd8df03877aa1bdd6bea7c5b9 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 13 May 2024 10:13:42 -0700 Subject: [PATCH 1/8] Initial commit for RelateNG Signed-off-by: Martin Davis Move to src dir Signed-off-by: Martin Davis More code Signed-off-by: Martin Davis Get it running Signed-off-by: Martin Davis Fix conflict Add more short-circuits Signed-off-by: Martin Davis Refactoring, add collinear intersection Signed-off-by: Martin Davis Add license, more predicates Signed-off-by: Martin Davis Renaming Signed-off-by: Martin Davis Renaming, fix L/L short-circuit Signed-off-by: Martin Davis WIP - more detailed intersection info Signed-off-by: Martin Davis Add geometry dimensionality checks Signed-off-by: Martin Davis Add LinearBoundary Signed-off-by: Martin Davis Add header, fix imports Rename IMPredicate methods Refactoring Refactoring remove dead code Refactor predicate model Refactor predicate model refactoring Add point support Simplify builder logic Fix proper intersection logic renaming code reorg Refactoring refactoring Fix order of EdgeIntersector comparison Add AreaArea crossing test Enhance PolygonNodeTopology to handle collinear Add predicates Javadoc Add node edge handling cleanup fix imports Add node evaluation Various improvements Various improvements Fix touches bug Renaming, fixes Renaming, fixes Improve tests Improve perf test Add PredicateTracer Add short-circuit Fix some bugs Renaming Refactoring refactoring Avoid check for empty element Fix area-vertex evaluation Remove unused import Renaming Renames Renames Refactor constants Renaming, refactoring refactoring refactoring rename TopologyPredicateValue renaming use constant renaming initial commit for self-noding Add RelateNG functions Refactor addAreaEdge Add AB geometry edge intersection test formatting Refactoring to simplify Remove single-call method Refactoring Renaming various improvements typo in comment various improvements refactoring Chg addEdge method sig Fix unit test Improve tracing output Finish self-intersection handling Expose constants javadoc, refactoring Switch to HPRtree Refactoring formatting Refactor predicate logic functions Improve predicate logic shortcut methods simplify code add method improve msg add tests Add relate function Various fixes change evaluation order Rework point topology evaluation refactoring refactoring javadoc, renaming remove dead code Improve relate predicate code improve internal API rename TopologyPredicate.value add perf tests refactoring Add BoundaryNodeRule support Fix SegmentString method usage Align with master Align with master Add headers Change classes to package-private Javadoc Improve TopologyPredicate API Refactor predicate API Refactor Predicate API Remove return value, rename javadoc Remove TopologyPredicateValue Refactor predicates renaming Add more functions Add RelateNG unit test base class Javadoc Refactoring, add GC handling various improvements Imrove GC point locating Add simple relate test renaming, testing renaming Improve node location computation Minor changes Introduce node sections to pass intersections refactoring Refactoring remove dead code refactoring TopologyBuilder refactoring Refactor topology logic simplify topology building logic reafactoring fix unit test Javadoc, refactoring refactor refactoring, modularization Add equalsTopo function Add polygon id Add tests doc refactoring Fix adjacent function Refactor out MaskPredicate Refactoring Fix RelateMaskPredicate logic cleanup Fix Relate mask predicate Improve node evaluation logic Add constants for relate patterns Fix adjacent test Improve RelateNG functions Improve AdjacentPolygons perf test Renaming Modularize NodeSections minor code cleanup Javadoc Regularize code Add tests Simplify intersection handling remove unused method disambiguate method Add node section inversion Improve NodeSection sorting refactoring, code cleanup Improve unit test Add PolygonNodeInverter unit test Generalize node conversion algorithm Fix headers Add PolygonNodeConverter unit test rename collect method javadoc Add NG functions code cleanup Javadoc Javadoc Code cleanup remove dead code Handle point location on GC adjacent edges Renaming Change to using MCIndexSegmentSetMutualIntersector Fix bug in AdjacentEdgeLocator Switch to using isOnSegment Minor code improvements Add unit test Change to package visible Fix how nodes are located on boundaries Javadoc Javadoc Add fix for overlapping GCs with hole Fix polygon touching interior endpoint Add area-line at point interaction update Javadoc, arg order Code cleanup, naming, refactoring Refactor Refactor DimensionLocation.EXTERIOR Implement prepared RelatePointLocator Change to using pattern from mask Change to pattern from mask Improve performance tests Improve point testing performance Javadoc Update copyright Add XML GC tests Add package Javadoc Update package Javadoc Rename RelateNG method Add RelatePatternPredicate short-circuit remove debug lines change method visibility Refactor RelateNG API Refactor API Move LinearBoundary into RelatePointLocator Refactor Locator Improve GC scanning Code cleanup, refactoring Javadoc Change API Refactor IM pattern API javadoc, method renaming Javadoc Refactoring renaming Renaming change visibility to package Renaming Javadoc Javavdoc Add Relate issue tests Add unit tests Improve Tracer API Cache geometry empty status Add potential optimization Add unit tests for zero-len line Remove bad test Javadoc Javadoc Fix handling of repeated points Javadoc Add optimization to skip disjoint geoms in point tests Fix EdgeSegmentIntersector deduplication Javadoc, refactoring Add unit test Add zero-length line test Javadoc Javadoc Javadoc --- .../function/SelectionNGFunctions.java | 80 +++ .../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 | 105 ++++ .../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 | 120 ++++ .../relateng/PolygonNodeConverter.java | 148 +++++ .../jts/operation/relateng/RelateEdge.java | 366 +++++++++++ .../operation/relateng/RelateGeometry.java | 353 +++++++++++ .../relateng/RelateMatrixPredicate.java | 50 ++ .../jts/operation/relateng/RelateNG.java | 493 +++++++++++++++ .../jts/operation/relateng/RelateNode.java | 232 +++++++ .../relateng/RelatePointLocator.java | 316 ++++++++++ .../operation/relateng/RelatePredicate.java | 563 +++++++++++++++++ .../relateng/RelateSegmentString.java | 158 +++++ .../operation/relateng/TopologyComputer.java | 502 +++++++++++++++ .../operation/relateng/TopologyPredicate.java | 117 ++++ .../relateng/TopologyPredicateTracer.java | 116 ++++ .../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 | 580 ++++++++++++++++++ .../operation/relateng/RelateNGTestCase.java | 94 +++ .../relateng/RelatePointLocatorTest.java | 87 +++ .../RelateNGPolygonPointsPerfTest.java | 177 ++++++ .../RelateNGPolygonsAdjacentPerfTest.java | 206 +++++++ .../RelateNGPolygonsOverlappingPerfTest.java | 199 ++++++ .../resources/testxml/misc/TestRelateGC.xml | 537 ++++++++++++++++ 43 files changed, 7573 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/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..cece1ee03b --- /dev/null +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionNGFunctions.java @@ -0,0 +1,80 @@ +/* + * 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 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..6330f3cddb --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPatternMatcher.java @@ -0,0 +1,105 @@ +/* + * 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"; } + + 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..9ee4a5c50b --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/NodeSections.java @@ -0,0 +1,120 @@ +/* + * 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()) { + int blockSize = 1; + 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); + blockSize = polySections.size(); + } + else { + //-- the most common case is a line or a single polygon ring section + node.addEdges(ns); + } + i += blockSize; + } + return node; + } + + private void prepareSections() { + /** + * Sort sections with lines before areas, + * and edges for polygons are together. + */ + 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..8c7335ed24 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateGeometry.java @@ -0,0 +1,353 @@ +/* + * 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 dim = Dimension.FALSE; + private List pts; + private Set uniquePoints; + private BoundaryNodeRule boundaryNodeRule; + private RelatePointLocator locator; + private int elementId = 0; + private boolean hasPoints; + private boolean hasLines; + private boolean hasAreas; + + 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; + dim = input.getDimension(); + analyzeDimensions(); + } + + private void analyzeDimensions() { + if (geom.isEmpty()) { + return; + } + if (geom instanceof Point || geom instanceof MultiPoint) { + hasPoints = true; + dim = Dimension.P; + return; + } + if (geom instanceof LineString || geom instanceof MultiLineString) { + hasLines = true; + dim = Dimension.L; + return; + } + if (geom instanceof Polygon || geom instanceof MultiPolygon) { + hasAreas = true; + dim = 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 (dim < Dimension.P) dim = Dimension.P; + } + if (elem instanceof LineString) { + hasLines = true; + if (dim < Dimension.L) dim = Dimension.L; + } + if (elem instanceof Polygon) { + hasAreas = true; + if (dim < Dimension.A) dim = Dimension.A; + } + } + } + + public Geometry getGeometry() { + return geom; + } + + public boolean isPrepared() { + return isPrepared; + } + + public Envelope getEnvelope() { + return geom.getEnvelopeInternal(); + } + + public int getDimension() { + return dim; + } + + 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 getDimensionEffective() { + if (geom.isEmpty()) return Dimension.FALSE; + if (getDimension() == 1 && geom.getLength() == 0) + return Dimension.P; + if (hasAreas) return Dimension.A; + if (hasLines) return Dimension.L; + return Dimension.P; + } + + public boolean hasEdges() { + return hasLines || hasAreas; + } + + public boolean isZeroLength() { + //TODO: evaluate component-wise and short-circuit + return geom.getLength() <= 0; + } + + 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 geom.isEmpty(); + } + + 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() { + //TODO: make more efficient (ie by scanning geometry?) + List pts = getCoordinates(); + Set set = new HashSet(); + set.addAll(pts); + return set; + } + + public List getCoordinates() { + if (pts == null) { + pts = ComponentCoordinateExtracter.getCoordinates(geom); + } + return pts; + } + + public List getEffectivePoints() { + List ptListAll = PointExtracter.getPoints(geom); + + if (getDimensionEffective() <= 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..7562811229 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java @@ -0,0 +1,493 @@ +/* + * 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.getDimensionEffective(); + int dimB = geomB.getDimensionEffective(); + + //-- 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 geomSrc, boolean isA, + RelateGeometry geomTarget, TopologyComputer topoComputer) { + + boolean isResultKnown = false; + isResultKnown = computePoints(geomSrc, isA, geomTarget, topoComputer); + if (isResultKnown) + return; + + isResultKnown = computeLineEnds(geomSrc, isA, geomTarget, topoComputer); + if (isResultKnown) + return; + + computeAreaVertices(geomSrc, 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 computeAreaVertices(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..99c6ed1692 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePredicate.java @@ -0,0 +1,563 @@ +/* + * 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 isSelfNodingRequired() { + //-- self-noding is not required to check for a simple 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 isSelfNodingRequired() { + //-- self-noding is not required to check for a simple 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 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 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 situations, 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 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 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 situations) + *
    • [T*****T**] (for L/P, A/P, and A/L situations) + *
    • [0********] (for L/L situations) + *
    + *
+ * For the A/A and P/P situations this predicate returns false. + *

+ * The SFS defined this predicate only for P/L, P/A, L/L, and L/A situations. + * To make the relation symmetric + * JTS extends the definition to apply to L/P, A/P and A/L situations 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 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 situations) + *
    • [T*****T**] (for L/P, A/P, and A/L situations) + *
    • [0********] (for L/L situations) + *
    + *
+ * For the A/A and P/P situations this predicate returns false. + *

+ * The SFS defined this predicate only for P/L, P/A, L/L, and L/A situations. + * To make the relation symmetric + * JTS extends the definition to apply to L/P, A/P and A/L situations as well. + * + * @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..e72e190942 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyComputer.java @@ -0,0 +1,502 @@ +/* + * 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 partial topology in the EXTERIORs a priori. + */ + private void initExteriorDims() { + int dimAEff = geomA.getDimensionEffective(); + int dimBEff = geomB.getDimensionEffective(); + + /** + * For P/L case, P exterior intersects L interior + */ + if (dimAEff == Dimension.P && dimBEff == Dimension.L) { + updateDim(Location.EXTERIOR, Location.INTERIOR, Dimension.L); + } + else if (dimAEff == Dimension.L && dimBEff == Dimension.P) { + updateDim(Location.INTERIOR, Location.EXTERIOR, Dimension.L); + } + /** + * For P/A case, the Area Int and Bdy intersect the Point exterior. + */ + else if (dimAEff == Dimension.P && dimBEff == Dimension.A) { + updateDim(Location.EXTERIOR, Location.INTERIOR, Dimension.A); + updateDim(Location.EXTERIOR, Location.BOUNDARY, Dimension.L); + } + else if (dimAEff == Dimension.A && dimBEff == Dimension.P) { + updateDim(Location.INTERIOR, Location.EXTERIOR, Dimension.A); + updateDim(Location.BOUNDARY, Location.EXTERIOR, Dimension.L); + } + else if (dimAEff == Dimension.L && dimBEff == Dimension.A) { + updateDim(Location.EXTERIOR, Location.INTERIOR, Dimension.A); + } + else if (dimAEff == Dimension.A && dimBEff == Dimension.L) { + updateDim(Location.INTERIOR, Location.EXTERIOR, Dimension.A); + } + //-- cases where one geom is EMPTY + else if (dimAEff == Dimension.FALSE || dimBEff == Dimension.FALSE) { + if (dimAEff != Dimension.FALSE) { + initExteriorEmpty(RelateGeometry.GEOM_A); + } + if (dimBEff != 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.isSelfNodingRequired(); + } + + 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..616c2f0559 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicate.java @@ -0,0 +1,117 @@ +/* + * 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(); + + /** + * Tests 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 isSelfNodingRequired() { + 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..8e478f4cc3 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateTracer.java @@ -0,0 +1,116 @@ +/* + * 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 isSelfNodingRequired() { + return pred.isSelfNodingRequired(); + } + + @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..a914ce9adf --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTest.java @@ -0,0 +1,580 @@ +/* + * 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 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/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 c39ae1e87a1e6379c50d3d867dba087b12b1bd52 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 13 May 2024 21:58:55 -0700 Subject: [PATCH 2/8] Rename local variable --- .../jts/operation/relateng/NodeSections.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) 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 index 9ee4a5c50b..c1a7ea96f0 100644 --- 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 @@ -68,29 +68,31 @@ public RelateNode createNode() { RelateNode node = new RelateNode(nodePt); int i = 0; while (i < sections.size()) { - int blockSize = 1; 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); - blockSize = polySections.size(); + i += polySections.size(); } else { //-- the most common case is a line or a single polygon ring section node.addEdges(ns); + i += 1; } - i += blockSize; } return node; } + /** + * Sorts the sections so that: + *
    + *
  • lines are before areas + *
  • edges from the same polygon are contiguous + *
+ */ private void prepareSections() { - /** - * Sort sections with lines before areas, - * and edges for polygons are together. - */ sections.sort(null); //TODO: remove duplicate sections } From 5ed9adf49c317b9234fbf6169ce5b94d53d12aee Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 15 May 2024 21:46:15 -0700 Subject: [PATCH 3/8] Add SelectionNG.contains --- .../jtstest/function/SelectionNGFunctions.java | 9 +++++++++ 1 file changed, 9 insertions(+) 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 cece1ee03b..ab2bf55dfc 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 @@ -38,6 +38,15 @@ public boolean isTrue(Geometry g) { }); } + 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() { From 76de567c83486b7a022fa469c0c49c1650597d68 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 15 May 2024 21:46:44 -0700 Subject: [PATCH 4/8] Optimize geometry zero-len line computation --- .../operation/relateng/RelateGeometry.java | 78 ++++++++++++------- 1 file changed, 50 insertions(+), 28 deletions(-) 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 8c7335ed24..7fc6dbb8b3 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,7 +48,7 @@ public static String name(boolean isA) { private Geometry geom; private boolean isPrepared = false; - private int dim = Dimension.FALSE; + private int geomDim = Dimension.FALSE; private List pts; private Set uniquePoints; private BoundaryNodeRule boundaryNodeRule; @@ -57,6 +57,8 @@ public static String name(boolean isA) { 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); @@ -70,27 +72,30 @@ public RelateGeometry(Geometry input, boolean isPrepared, BoundaryNodeRule bnRul this.geom = input; this.isPrepared = isPrepared; this.boundaryNodeRule = bnRule; - dim = input.getDimension(); + //-- cache geometry metadata + isGeomEmpty = geom.isEmpty(); + isLineZeroLen = isZeroLength(geom); + geomDim = input.getDimension(); analyzeDimensions(); } private void analyzeDimensions() { - if (geom.isEmpty()) { + if (isGeomEmpty) { return; } if (geom instanceof Point || geom instanceof MultiPoint) { hasPoints = true; - dim = Dimension.P; + geomDim = Dimension.P; return; } if (geom instanceof LineString || geom instanceof MultiLineString) { hasLines = true; - dim = Dimension.L; + geomDim = Dimension.L; return; } if (geom instanceof Polygon || geom instanceof MultiPolygon) { hasAreas = true; - dim = Dimension.A; + geomDim = Dimension.A; return; } //-- analyze a (possibly mixed type) collection @@ -101,19 +106,48 @@ private void analyzeDimensions() { continue; if (elem instanceof Point) { hasPoints = true; - if (dim < Dimension.P) dim = Dimension.P; + if (geomDim < Dimension.P) geomDim = Dimension.P; } if (elem instanceof LineString) { hasLines = true; - if (dim < Dimension.L) dim = Dimension.L; + if (geomDim < Dimension.L) geomDim = Dimension.L; } if (elem instanceof Polygon) { hasAreas = true; - if (dim < Dimension.A) dim = Dimension.A; - } + if (geomDim < Dimension.A) geomDim = Dimension.A; + } } } + /** + * Tests if geometry linear elements are zero-length. + * The test is optimized to + * avoid computing length in the common case, since that is expensive. + * + * @param geom + * @return + */ + private boolean isZeroLength(Geometry geom) { + Iterator geomi = new GeometryCollectionIterator(geom); + while (geomi.hasNext()) { + Geometry elem = (Geometry) geomi.next(); + if (elem instanceof LineString) { + LineString line = (LineString) elem; + if (line.getNumPoints() >= 2) { + Coordinate p0 = line.getCoordinateN(0); + Coordinate p1 = line.getCoordinateN(1); + //-- the usual non-zero-length case will have first two points non-equal + if (! p0.equals2D(p1) + || line.getLength() > 0) { + return false; + } + } + } + } + return true; + } + + public Geometry getGeometry() { return geom; } @@ -127,7 +161,7 @@ public Envelope getEnvelope() { } public int getDimension() { - return dim; + return geomDim; } public boolean hasDimension(int dim) { @@ -140,8 +174,8 @@ public boolean hasDimension(int dim) { } public int getDimensionEffective() { - if (geom.isEmpty()) return Dimension.FALSE; - if (getDimension() == 1 && geom.getLength() == 0) + if (isGeomEmpty) return Dimension.FALSE; + if (getDimension() == 1 && isLineZeroLen) return Dimension.P; if (hasAreas) return Dimension.A; if (hasLines) return Dimension.L; @@ -152,11 +186,6 @@ public boolean hasEdges() { return hasLines || hasAreas; } - public boolean isZeroLength() { - //TODO: evaluate component-wise and short-circuit - return geom.getLength() <= 0; - } - private RelatePointLocator getLocator() { if (locator == null) locator = new RelatePointLocator(geom, isPrepared, boundaryNodeRule); @@ -219,7 +248,7 @@ public boolean isPolygonal() { } public boolean isEmpty() { - return geom.isEmpty(); + return isGeomEmpty; } public boolean hasBoundary() { @@ -235,19 +264,12 @@ public Set getUniquePoints() { } private Set createUniquePoints() { - //TODO: make more efficient (ie by scanning geometry?) - List pts = getCoordinates(); + //-- only called on P geometries + List pts = ComponentCoordinateExtracter.getCoordinates(geom); Set set = new HashSet(); set.addAll(pts); return set; } - - public List getCoordinates() { - if (pts == null) { - pts = ComponentCoordinateExtracter.getCoordinates(geom); - } - return pts; - } public List getEffectivePoints() { List ptListAll = PointExtracter.getPoints(geom); From 922e06f0878dc6f2bf6c2ff1858e9069c43afd50 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 16 May 2024 12:40:55 -0700 Subject: [PATCH 5/8] Fix Javadoc --- .../operation/relateng/RelatePredicate.java | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) 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 99c6ed1692..a02ec9de0f 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 @@ -248,7 +248,7 @@ public boolean valueIM() { * 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 situations, covers should be used in preference to contains. + * 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. * @@ -346,16 +346,16 @@ public boolean valueIM() { *
  • 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 situations) - *
    • [T*****T**] (for L/P, A/P, and A/L situations) - *
    • [0********] (for L/L situations) + *
    • [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 situations this predicate returns false. + * 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 situations. + * 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 situations as well. + * JTS extends the definition to apply to L/P, A/P and A/L cases as well. * * @return the predicate instance */ @@ -451,20 +451,17 @@ public boolean valueIM() { *

    * The overlaps predicate has the following equivalent definitions: *

      - *
    • The geometries have some but not all interior points in common. + *
    • 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 - * one of the following patterns: - *
        - *
      • [T*T******] (for P/L, P/A, and L/A situations) - *
      • [T*****T**] (for L/P, A/P, and A/L situations) - *
      • [0********] (for L/L situations) - *
      + * [T*T***T**] (for P/P and A/A cases) + * or [1*T***T**] (for L/L cases) *
    - * For the A/A and P/P situations this predicate returns false. - *

    - * The SFS defined this predicate only for P/L, P/A, L/L, and L/A situations. - * To make the relation symmetric - * JTS extends the definition to apply to L/P, A/P and A/L situations as well. + * If the geometries are of different dimension this predicate returns false. + * This predicate is symmetric. * * @return the predicate instance */ From ace39ee2cb9cbbf382005b119071066beb1fdd2e Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 16 May 2024 12:41:26 -0700 Subject: [PATCH 6/8] Add Lines perf test --- .../RelateNGLinesOverlappingPerfTest.java | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGLinesOverlappingPerfTest.java 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; + } + + +} From 861c38396c6bfdc776e7422e743e79ca77061507 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 16 May 2024 12:42:57 -0700 Subject: [PATCH 7/8] Renames, cleanup --- .../operation/relateng/RelateGeometry.java | 37 ++++++++++--------- .../jts/operation/relateng/RelateNG.java | 18 ++++----- .../operation/relateng/TopologyComputer.java | 24 ++++++------ 3 files changed, 41 insertions(+), 38 deletions(-) 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 7fc6dbb8b3..005ddd6184 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 @@ -49,7 +49,6 @@ public static String name(boolean isA) { private boolean isPrepared = false; private int geomDim = Dimension.FALSE; - private List pts; private Set uniquePoints; private BoundaryNodeRule boundaryNodeRule; private RelatePointLocator locator; @@ -120,28 +119,32 @@ private void analyzeDimensions() { } /** - * Tests if geometry linear elements are zero-length. - * The test is optimized to - * avoid computing length in the common case, since that is expensive. + * Tests if all geometry linear elements are zero-length. + * For efficiency the test avoids computing actual length. * * @param geom * @return */ - private boolean isZeroLength(Geometry geom) { + private static boolean isZeroLength(Geometry geom) { Iterator geomi = new GeometryCollectionIterator(geom); while (geomi.hasNext()) { Geometry elem = (Geometry) geomi.next(); if (elem instanceof LineString) { - LineString line = (LineString) elem; - if (line.getNumPoints() >= 2) { - Coordinate p0 = line.getCoordinateN(0); - Coordinate p1 = line.getCoordinateN(1); - //-- the usual non-zero-length case will have first two points non-equal - if (! p0.equals2D(p1) - || line.getLength() > 0) { - return false; - } - } + 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; @@ -173,7 +176,7 @@ public boolean hasDimension(int dim) { return false; } - public int getDimensionEffective() { + public int getDimensionReal() { if (isGeomEmpty) return Dimension.FALSE; if (getDimension() == 1 && isLineZeroLen) return Dimension.P; @@ -274,7 +277,7 @@ private Set createUniquePoints() { public List getEffectivePoints() { List ptListAll = PointExtracter.getPoints(geom); - if (getDimensionEffective() <= Dimension.P) + if (getDimensionReal() <= Dimension.P) return ptListAll; //-- only return Points not covered by another element 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 7562811229..6fb4cf3d55 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 @@ -227,8 +227,8 @@ public boolean evaluate(Geometry b, TopologyPredicate predicate) { //TODO: what if predicate is disjoint? Perhaps use result on disjoint envs? return finishValue(predicate); } - int dimA = geomA.getDimensionEffective(); - int dimB = geomB.getDimensionEffective(); + int dimA = geomA.getDimensionReal(); + int dimB = geomB.getDimensionReal(); //-- check if predicate is determined by dimension or envelope predicate.init(dimA, dimB); @@ -307,19 +307,19 @@ private void computePP(RelateGeometry geomB, TopologyComputer topoComputer) { } } - private void computeAtPoints(RelateGeometry geomSrc, boolean isA, + private void computeAtPoints(RelateGeometry geom, boolean isA, RelateGeometry geomTarget, TopologyComputer topoComputer) { boolean isResultKnown = false; - isResultKnown = computePoints(geomSrc, isA, geomTarget, topoComputer); + isResultKnown = computePoints(geom, isA, geomTarget, topoComputer); if (isResultKnown) return; - isResultKnown = computeLineEnds(geomSrc, isA, geomTarget, topoComputer); + isResultKnown = computeLineEnds(geom, isA, geomTarget, topoComputer); if (isResultKnown) return; - computeAreaVertices(geomSrc, isA, geomTarget, topoComputer); + computeAreaVertex(geom, isA, geomTarget, topoComputer); } private boolean computePoints(RelateGeometry geom, boolean isA, RelateGeometry geomTarget, @@ -364,7 +364,7 @@ private boolean computeLineEnds(RelateGeometry geom, boolean isA, RelateGeometry continue; if (elem instanceof LineString) { - //-- once an intersection with target exterior is recorded, skip further known exterior points + //-- once an intersection with target exterior is recorded, skip further known-exterior points if (hasExteriorIntersection && elem.getEnvelopeInternal().disjoint(geomTarget.getEnvelope())) continue; @@ -400,7 +400,7 @@ private boolean computeLineEnd(RelateGeometry geom, boolean isA, Coordinate pt, return locTarget == Location.EXTERIOR; } - private boolean computeAreaVertices(RelateGeometry geom, boolean isA, RelateGeometry geomTarget, TopologyComputer topoComputer) { + private boolean computeAreaVertex(RelateGeometry geom, boolean isA, RelateGeometry geomTarget, TopologyComputer topoComputer) { if (! geom.hasDimension(Dimension.A)) { return false; } @@ -416,7 +416,7 @@ private boolean computeAreaVertices(RelateGeometry geom, boolean isA, RelateGeom continue; if (elem instanceof Polygon) { - //-- once an intersection with target exterior is recorded, skip further known exterior points + //-- once an intersection with target exterior is recorded, skip further known-exterior points if (hasExteriorIntersection && elem.getEnvelopeInternal().disjoint(geomTarget.getEnvelope())) continue; 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 e72e190942..06f686ed80 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 @@ -39,44 +39,44 @@ public TopologyComputer(TopologyPredicate predicate, RelateGeometry geomA, Relat } /** - * Determine partial topology in the EXTERIORs a priori. + * Determine a priori partial EXTERIOR topology based on dimensions. */ private void initExteriorDims() { - int dimAEff = geomA.getDimensionEffective(); - int dimBEff = geomB.getDimensionEffective(); + int dimRealA = geomA.getDimensionReal(); + int dimRealB = geomB.getDimensionReal(); /** * For P/L case, P exterior intersects L interior */ - if (dimAEff == Dimension.P && dimBEff == Dimension.L) { + if (dimRealA == Dimension.P && dimRealB == Dimension.L) { updateDim(Location.EXTERIOR, Location.INTERIOR, Dimension.L); } - else if (dimAEff == Dimension.L && dimBEff == Dimension.P) { + 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 (dimAEff == Dimension.P && dimBEff == Dimension.A) { + else if (dimRealA == Dimension.P && dimRealB == Dimension.A) { updateDim(Location.EXTERIOR, Location.INTERIOR, Dimension.A); updateDim(Location.EXTERIOR, Location.BOUNDARY, Dimension.L); } - else if (dimAEff == Dimension.A && dimBEff == Dimension.P) { + else if (dimRealA == Dimension.A && dimRealB == Dimension.P) { updateDim(Location.INTERIOR, Location.EXTERIOR, Dimension.A); updateDim(Location.BOUNDARY, Location.EXTERIOR, Dimension.L); } - else if (dimAEff == Dimension.L && dimBEff == Dimension.A) { + else if (dimRealA == Dimension.L && dimRealB == Dimension.A) { updateDim(Location.EXTERIOR, Location.INTERIOR, Dimension.A); } - else if (dimAEff == Dimension.A && dimBEff == Dimension.L) { + else if (dimRealA == Dimension.A && dimRealB == Dimension.L) { updateDim(Location.INTERIOR, Location.EXTERIOR, Dimension.A); } //-- cases where one geom is EMPTY - else if (dimAEff == Dimension.FALSE || dimBEff == Dimension.FALSE) { - if (dimAEff != Dimension.FALSE) { + else if (dimRealA == Dimension.FALSE || dimRealB == Dimension.FALSE) { + if (dimRealA != Dimension.FALSE) { initExteriorEmpty(RelateGeometry.GEOM_A); } - if (dimBEff != Dimension.FALSE) { + if (dimRealB != Dimension.FALSE) { initExteriorEmpty(RelateGeometry.GEOM_B); } } From 5b29e1d90814ef9c72d9bf973b63d8239c1bb372 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 16 May 2024 13:41:31 -0700 Subject: [PATCH 8/8] Add predicate metadata for Exterior check --- .../operation/relateng/IMPatternMatcher.java | 2 + .../jts/operation/relateng/RelateNG.java | 12 ++++++ .../operation/relateng/RelatePredicate.java | 42 +++++++++++++++++-- .../operation/relateng/TopologyComputer.java | 6 ++- .../operation/relateng/TopologyPredicate.java | 16 ++++++- .../relateng/TopologyPredicateTracer.java | 9 +++- .../jts/operation/relateng/RelateNGTest.java | 9 ++++ 7 files changed, 88 insertions(+), 8 deletions(-) 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 6330f3cddb..920dc4d124 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 @@ -43,6 +43,8 @@ public IMPatternMatcher(String 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 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 6fb4cf3d55..977fd75637 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 @@ -315,6 +315,18 @@ private void computeAtPoints(RelateGeometry geom, boolean isA, 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; 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 a02ec9de0f..6eef9ba41f 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,11 +52,17 @@ public static TopologyPredicate intersects() { public String name() { return "intersects"; } @Override - public boolean isSelfNodingRequired() { + 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)); @@ -98,11 +104,17 @@ public static TopologyPredicate disjoint() { public String name() { return "disjoint"; } @Override - public boolean isSelfNodingRequired() { + 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)); @@ -151,6 +163,12 @@ public static TopologyPredicate contains() { 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); @@ -202,7 +220,13 @@ 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); @@ -261,6 +285,12 @@ public static TopologyPredicate covers() { 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); @@ -314,6 +344,12 @@ 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); 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 06f686ed80..1fd7d81880 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,7 +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.isSelfNodingRequired(); + return predicate.requiresSelfNoding(); + } + + public boolean isExteriorCheckRequired(boolean isA) { + return predicate.requiresExteriorCheck(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 616c2f0559..639427ede9 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 @@ -30,7 +30,7 @@ public interface TopologyPredicate { String name(); /** - * Tests whether this predicate requires self-noding for + * 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). @@ -45,10 +45,22 @@ public interface TopologyPredicate { * * @return true if self-noding is required. */ - default boolean isSelfNodingRequired() { + 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 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 8e478f4cc3..416e21a799 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,8 +47,13 @@ private PredicateTracer(TopologyPredicate pred) { public String name() { return pred.name(); } @Override - public boolean isSelfNodingRequired() { - return pred.isSelfNodingRequired(); + public boolean requiresSelfNoding() { + return pred.requiresSelfNoding(); + } + + @Override + public boolean requiresExteriorCheck(boolean isSourceA) { + return pred.requiresExteriorCheck(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 a914ce9adf..1ba16d1c63 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 @@ -184,6 +184,15 @@ public void testLinesTouchAtBdy() { 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)";