From 5e1e44c0b3ee6f01a1204dcc64169075c78fab13 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Fri, 19 Jan 2024 11:53:30 -0800 Subject: [PATCH] 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 --- .../function/SelectionNGFunctions.java | 60 ++ .../function/SpatialPredicateNGFunctions.java | 53 ++ .../GeometryFunctionRegistry.java | 4 + .../relateng/EdgeSegmentIntersector.java | 207 +++++++ .../relateng/EdgeSegmentOverlapAction.java | 25 + .../relateng/EdgeSetIntersector.java | 71 +++ .../relateng/EdgeSetMutualIntersector.java | 119 ++++ .../jts/operation/relateng/IMPredicate.java | 100 ++++ .../operation/relateng/LinearBoundary.java | 84 +++ .../jts/operation/relateng/RelateEdge.java | 250 ++++++++ .../operation/relateng/RelateGeometry.java | 293 ++++++++++ .../jts/operation/relateng/RelateNG.java | 314 ++++++++++ .../jts/operation/relateng/RelateNode.java | 165 ++++++ .../operation/relateng/RelatePredicate.java | 38 ++ .../operation/relateng/SimplePredicate.java | 52 ++ .../operation/relateng/TopologyBuilder.java | 431 ++++++++++++++ .../operation/relateng/TopologyPredicate.java | 40 ++ .../relateng/TopologyPredicateFactory.java | 347 +++++++++++ .../relateng/TopologyPredicateTracer.java | 56 ++ .../relateng/TopologyPredicateValue.java | 22 + .../RelateNGBoundaryNodeRuleTest.java | 131 +++++ .../jts/operation/relateng/RelateNGTest.java | 549 ++++++++++++++++++ .../RelateNGPolygonPointsPerfTest.java | 177 ++++++ .../RelateNGPolygonsAdjacentPerfTest.java | 148 +++++ .../RelateNGPolygonsOverlappingPerfTest.java | 181 ++++++ 25 files changed, 3917 insertions(+) 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/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/EdgeSetMutualIntersector.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/LinearBoundary.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/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/RelatePredicate.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/SimplePredicate.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyBuilder.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/TopologyPredicateFactory.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/TopologyPredicateValue.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/RelateNGTest.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 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..9bf6df14fd --- /dev/null +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionNGFunctions.java @@ -0,0 +1,60 @@ +/* + * 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.RelateNG; +import org.locationtech.jts.operation.relateng.TopologyPredicateFactory; + +public class SelectionNGFunctions +{ + public static Geometry intersectsNG(Geometry a, final Geometry mask) + { + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return RelateNG.evaluate(TopologyPredicateFactory.intersects(), mask, g); + } + }); + } + + public static Geometry intersectsNGPrep(Geometry a, final Geometry mask) + { + RelateNG relateNG = new RelateNG(mask, true); + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return relateNG.evaluate(TopologyPredicateFactory.intersects(), g); + } + }); + } + + public static Geometry coversNG(Geometry a, final Geometry mask) + { + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return RelateNG.evaluate(TopologyPredicateFactory.covers(), mask, g); + } + }); + } + + public static Geometry coversNGPrep(Geometry a, final Geometry mask) + { + RelateNG relateNG = new RelateNG(mask, true); + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return relateNG.evaluate(TopologyPredicateFactory.covers(), g); + } + }); + } +} + + 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..9fefe11189 --- /dev/null +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/SpatialPredicateNGFunctions.java @@ -0,0 +1,53 @@ +/* + * 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.geom.Geometry; +import org.locationtech.jts.operation.relateng.RelateNG; +import org.locationtech.jts.operation.relateng.TopologyPredicateFactory; + +public class SpatialPredicateNGFunctions { + public static boolean contains(Geometry a, Geometry b) { + return RelateNG.evaluate(TopologyPredicateFactory.contains(), a, b); + } + public static boolean covers(Geometry a, Geometry b) { + return RelateNG.evaluate(TopologyPredicateFactory.covers(), a, b); + } + public static boolean coveredBy(Geometry a, Geometry b) { + return RelateNG.evaluate(TopologyPredicateFactory.coveredBy(), a, b); + } + public static boolean disjoint(Geometry a, Geometry b) { + return RelateNG.evaluate(TopologyPredicateFactory.disjoint(), a, b); + } + public static boolean equals(Geometry a, Geometry b) { + return RelateNG.evaluate(TopologyPredicateFactory.equalsTopo(), a, b); + } + public static boolean intersects(Geometry a, Geometry b) { + return RelateNG.evaluate(TopologyPredicateFactory.intersects(), a, b); + } + public static boolean overlaps(Geometry a, Geometry b) { + return RelateNG.evaluate(TopologyPredicateFactory.overlaps(), a, b); + } + public static boolean touches(Geometry a, Geometry b) { + return RelateNG.evaluate(TopologyPredicateFactory.touches(), a, b); + } + public static boolean within(Geometry a, Geometry b) { + return RelateNG.evaluate(TopologyPredicateFactory.within(), a, b); + } + public static boolean relate(Geometry a, Geometry b, String mask) { + return RelateNG.relate(a, b, mask); + } + public static String relateIM(Geometry a, Geometry b) { + return RelateNG.relate(a, b).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/operation/relateng/EdgeSegmentIntersector.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSegmentIntersector.java new file mode 100644 index 0000000000..d1c2d4af3a --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSegmentIntersector.java @@ -0,0 +1,207 @@ +/* + * 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; +import org.locationtech.jts.noding.SegmentStringUtil; + +public class EdgeSegmentIntersector implements SegmentIntersector +{ + private RobustLineIntersector li = new RobustLineIntersector(); + private TopologyBuilder topoBuilder; + + public EdgeSegmentIntersector(TopologyBuilder topoBuilder) { + this.topoBuilder = topoBuilder; + } + + @Override + public boolean isDone() { + return topoBuilder.isResultKnown(); + } + + public void processIntersections(SegmentString ss0, int segIndex0, SegmentString ss1, int segIndex1) { + if (isAB(ss0, ss1)) { + if (isA(ss0)) { + processIntersectionAB(ss0, segIndex0, ss1, segIndex1); + } + else { + processIntersectionAB(ss1, segIndex1, ss0, segIndex0); + } + } + else { + processSelfIntersection(ss0, segIndex0, ss1, segIndex1); + } + } + + private void processSelfIntersection(SegmentString ss0, int segIndex0, SegmentString ss1, int segIndex1) { + // don't intersect a segment with itself + if (ss0 == ss1 && segIndex0 == segIndex1) return; + + //TODO: skip intersections between adjacent segments? + + Coordinate p00 = ss0.getCoordinate(segIndex0); + Coordinate p01 = ss0.getCoordinate(segIndex0 + 1); + Coordinate p10 = ss1.getCoordinate(segIndex1); + Coordinate p11 = ss1.getCoordinate(segIndex1 + 1); + + li.computeIntersection(p00, p01, p10, p11); + + if (! li.hasIntersection()) + return; + + boolean isA = isA(ss0); + if (li.getIntersectionNum() == 2) { + //-- intersection is collinear + topoBuilder.addSelfIntersectionCollinear(isA, p00, p01, p10, p11, li.getIntersection(0), li.getIntersection(1)); + } + else if (li.isProper()) { + //-- intersection is proper + topoBuilder.addSelfIntersectionProper(isA, p00, p01, p10, p11, li.getIntersection(0)); + } + else { + //-- non-proper intersection (at least one segment intersects at endpoint) + addSelfIntersectionNonProper(isA, ss0, segIndex0, ss1, segIndex1, li.getIntersection(0)); + } + } + + private boolean isAB(SegmentString ss0, SegmentString ss1) { + RelateGeometry geom0 = (RelateGeometry) ss0.getData(); + RelateGeometry geom1 = (RelateGeometry) ss1.getData(); + return geom0 != geom1; + } + + private boolean isA(SegmentString ss0) { + return topoBuilder.isA((RelateGeometry) ss0.getData()); + } + + private void processIntersectionAB(SegmentString ssA, int segIndexA, SegmentString 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; + + if (li.getIntersectionNum() == 2) { + //-- intersection is collinear + topoBuilder.addIntersectionCollinear(a0, a1, b0, b1, li.getIntersection(0), li.getIntersection(1)); + } + else if (li.isProper()) { + //-- intersection is proper + topoBuilder.addIntersectionProper(a0, a1, b0, b1, li.getIntersection(0)); + } + else { + //-- non-proper intersection (at least one segment intersects at endpoint) + addIntersectionNonProper(ssA, segIndexA, ssB, segIndexB, li.getIntersection(0)); + } + } + + private void addSelfIntersectionNonProper(boolean isA, SegmentString ss0, int segIndex0, SegmentString ss1, int segIndex1, Coordinate intPt) { + //-- this handles A/L, L/A, and L/L + addIntersectionEdges(isA, ss0, segIndex0, intPt); + addIntersectionEdges(isA, ss1, segIndex1, intPt); + } + + private void addIntersectionNonProper(SegmentString ssA, int segIndexA, SegmentString ssB, int segIndexB, Coordinate intPt) { + if (topoBuilder.isAreaArea()) { + addNonProperAreaArea(ssA, segIndexA, ssB, segIndexB, intPt); + return; + } + //-- this handles A/L, L/A, and L/L + addIntersectionEdges(RelateGeometry.GEOM_A, ssA, segIndexA, intPt); + addIntersectionEdges(RelateGeometry.GEOM_B, ssB, segIndexB, intPt); + + //TODO: more logic required? + //TODO: move to topoBuilder + topoBuilder.addEdgeIntersectionNode(intPt); + } + + private void addIntersectionEdges(boolean isA, SegmentString ss, int segIndex, Coordinate intPt) { + Coordinate nextVertex = nextVertex(ss, segIndex, intPt); + topoBuilder.addEdge(isA, intPt, nextVertex, RelateEdge.IS_FORWARD); + Coordinate prevVertex = prevVertex(ss, segIndex, intPt); + topoBuilder.addEdge(isA, intPt, prevVertex, RelateEdge.IS_REVERSE); + } + + /** + * + * @param ss + * @param segIndex + * @param pt + * @return the previous vertex, or null if none exists + */ + private static Coordinate prevVertex(SegmentString ss, int segIndex, Coordinate pt) { + Coordinate segStart = ss.getCoordinate(segIndex); + if (! segStart.equals2D(pt)) + return segStart; + //-- pt is at segment start, so get previous vertex + if (segIndex > 0) + return ss.getCoordinate(segIndex - 1); + if (ss.isClosed()) + return ss.prevInRing(segIndex); + return null; + } + + /** + * + * @param ss + * @param segIndex + * @param pt + * @return the next vertex, or null if none exists + */ + private static Coordinate nextVertex(SegmentString ss, int segIndex, Coordinate pt) { + Coordinate segEnd = ss.getCoordinate(segIndex + 1); + if (! segEnd.equals2D(pt)) + return segEnd; + //-- pt is at seg end, so get next vertex + if (segIndex < ss.size() - 2) + return ss.getCoordinate(segIndex + 2); + if (ss.isClosed()) + return ss.nextInRing(segIndex); + //-- segstring is not closed, so there is no next segment + return null; + } + + private void addNonProperAreaArea(SegmentString ssA, int segIndexA, SegmentString ssB, int segIndexB, + Coordinate intPt) { + Coordinate[] adjPtsA = adjacentRingVertices(ssA, segIndexA, intPt); + Coordinate[] adjPtsB = adjacentRingVertices(ssB, segIndexB, intPt); + topoBuilder.addAreaAreaIntersection(false, adjPtsA[0], adjPtsA[1], adjPtsB[0], adjPtsB[1], intPt); + } + + private static Coordinate[] adjacentRingVertices(SegmentString ssRing, int segIndex, Coordinate intPt) { + Coordinate p0 = ssRing.getCoordinate(segIndex); + Coordinate p1 = ssRing.getCoordinate(segIndex + 1); + Coordinate[] adjPts = new Coordinate[2]; + if (intPt.equals2D(p0)) { + adjPts[0] = ssRing.prevInRing(segIndex); + adjPts[1] = p1; + } + else if (intPt.equals2D(p1)) { + adjPts[0] = p0; + adjPts[1] = ssRing.nextInRing(segIndex + 1); + } + else { + //-- intersection is in interior of segment + adjPts[0] = p0; + adjPts[1] = p1; + } + return adjPts; + } +} 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..6d43bbc80f --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSegmentOverlapAction.java @@ -0,0 +1,25 @@ +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; + +public 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..68ba273dac --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSetIntersector.java @@ -0,0 +1,71 @@ +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; + +public 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/EdgeSetMutualIntersector.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSetMutualIntersector.java new file mode 100644 index 0000000000..61c048a067 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSetMutualIntersector.java @@ -0,0 +1,119 @@ +/* + * 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.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.SegmentIntersector; +import org.locationtech.jts.noding.SegmentSetMutualIntersector; +import org.locationtech.jts.noding.SegmentString; + + +/** + * Intersects two sets of {@link SegmentString}s using a spatial index based + * on {@link MonotoneChain}s. + * + * Thread-safe and immutable. + * + * @version 1.7 + */ +public class EdgeSetMutualIntersector implements SegmentSetMutualIntersector +{ + private HPRtree index = new HPRtree(); + private Envelope envelope; + + /** + * Constructs a new intersector for a set of {@link SegmentString}s. + * + * @param baseEdges the base segment strings to intersect + * @param env the extent of segments to process + */ + public EdgeSetMutualIntersector(Collection baseEdges, Envelope env) + { + this.envelope = env; + initBase(baseEdges); + } + + private void initBase(Collection segStrings) + { + for (SegmentString ss : segStrings) { + addToIndex(ss); + } + // build index to ensure thread-safety + index.build(); + } + + 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())) { + index.insert(mc.getEnvelope(), mc); + } + } + } + + /** + * Calls {@link SegmentIntersector#processIntersections(SegmentString, int, SegmentString, int)} + * for all candidate intersections between + * the given collection of SegmentStrings and the set of indexed segments. + * + * @param edges set of segments to intersect + * @param segInt segment intersector to use + */ + public void process(Collection edges, SegmentIntersector segInt) + { + List monoChains = new ArrayList(); + for (Iterator i = edges.iterator(); i.hasNext(); ) { + addToMonoChains((SegmentString) i.next(), monoChains); + } + intersectChains(monoChains, segInt); +// System.out.println("MCIndexBichromaticIntersector: # chain overlaps = " + nOverlaps); +// System.out.println("MCIndexBichromaticIntersector: # oct chain overlaps = " + nOctOverlaps); + } + + 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(); + if (envelope == null || envelope.intersects(mc.getEnvelope())) { + monoChains.add(mc); + } + } + } + + private void intersectChains(List monoChains, SegmentIntersector segInt) + { + MonotoneChainOverlapAction overlapAction = new EdgeSegmentOverlapAction(segInt); + + for (Iterator i = monoChains.iterator(); i.hasNext(); ) { + MonotoneChain queryChain = (MonotoneChain) i.next(); + List overlapChains = index.query(queryChain.getEnvelope()); + for (Iterator j = overlapChains.iterator(); j.hasNext(); ) { + MonotoneChain testChain = (MonotoneChain) j.next(); + testChain.computeOverlaps(queryChain, overlapAction); + if (segInt.isDone()) + return; + } + } + } +} 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..ed92491dc3 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPredicate.java @@ -0,0 +1,100 @@ +/* + * 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; + +public abstract class IMPredicate implements TopologyPredicate { + + static final int DIM_UNKNOWN = Dimension.DONTCARE; + + protected IntersectionMatrix intMatrix; + + public IMPredicate() { + //TODO: add initializer for IntersectionMatrix to a single value? + //intMatrix = new IntersectionMatrix("*********"); + intMatrix = new IntersectionMatrix(); + //-- E/E is always dim = 2 + intMatrix.set(Location.EXTERIOR, Location.EXTERIOR, Dimension.A); + } + + /** + * Gets the current state of the IM matrix (which may only be partially complete). + * + * @return the IM matrix + */ + protected IntersectionMatrix getIM() { + return intMatrix; + } + + @Override + public void updateDim(int locA, int locB, int dimension) { + //-- only record an increased dimension value + if (dimension <= intMatrix.get(locA, locB)) + return; + + intMatrix.set(locA, locB, dimension); + } + + 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; + } + + protected boolean isUnknown(int locA, int locB) { + return intMatrix.get(locA, locB) == DIM_UNKNOWN; + } + + public boolean isKnown(int locA, int locB) { + return intMatrix.get(locA, locB) != DIM_UNKNOWN; + } + + public boolean isDim(int locA, int locB, int dimension) { + return intMatrix.get(locA, locB) == dimension; + } + + public int getDim(int locA, int locB) { + return intMatrix.get(locA, locB); + } + + /** + * Finalizes the matrix by setting UNKNOWN values to appropriate values. + */ + public void finish() { + //TODO: is this needed? + /* + for (int ia = 0; ia < 3; ia++) { + for (int ib = 0; ib < 3; ib++) { + if (isUnknown(ia, ib)) + intMatrix.set(ia, ib, Dimension.FALSE); + } + } + */ + } + + public String toString() { + return name() + ": " + intMatrix; + } + +} 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..7eafbbaa5e --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/LinearBoundary.java @@ -0,0 +1,84 @@ +/* + * 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 java.util.Set; + +import org.locationtech.jts.algorithm.BoundaryNodeRule; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LineString; + +public class LinearBoundary { + + //TODO: handle BoundaryNodeRule + + private Map vertexDegree = new HashMap(); + private boolean hasBoundary; + private BoundaryNodeRule boundaryNodeRule; + + public LinearBoundary(Geometry geom, BoundaryNodeRule bnRule) { + //assert: dim(geom) == 1 + this.boundaryNodeRule = bnRule; + vertexDegree = computeBoundaryPoints(geom); + 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); + //TODO: add support for settable BoundaryNodeRule + return boundaryNodeRule.isInBoundary(degree); + } + + private static Map computeBoundaryPoints(Geometry geom) { + Map vertexDegree = new HashMap(); + for (int i = 0; i < geom.getNumGeometries(); i++) { + LineString line = (LineString) geom.getGeometryN(i); + 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/RelateEdge.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateEdge.java new file mode 100644 index 0000000000..79573ce1f0 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateEdge.java @@ -0,0 +1,250 @@ +/* + * 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.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; + + /** + * The dimension of an input geometry which is not known + */ + private 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; + + public RelateEdge(RelateNode node, Coordinate pt, boolean isA, int locLeft, int locRight) { + this.node = node; + this.dirPt = pt; + setLocationsArea(isA, locLeft, locRight); + } + + public RelateEdge(RelateNode node, Coordinate pt, boolean isA, int locLine) { + this.node = node; + this.dirPt = pt; + setLocationsLine(isA, locLine); + } + + 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, int locLine) { + if (isA) { + aDim = 1; + aLocLeft = Location.EXTERIOR; + aLocRight = Location.EXTERIOR; + aLocLine = locLine; + } + else { + bDim = 1; + bLocLeft = Location.EXTERIOR; + bLocRight = Location.EXTERIOR; + bLocLine = locLine; + } + } + private void setLocationsArea(boolean isA, int locLeft, int locRight) { + 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 edgePt) { + return PolygonNodeTopology.compareAngle(node.getCoordinate(), dirPt, edgePt); + } + + public void merge(Coordinate pt, boolean isA, int locLeft, int locRight, int locLine) { + // Assert: node-dirpt is collinear with node-pt + // Assert: locLeft is opposite of locRight + int dim = locLine == Location.BOUNDARY ? Dimension.A : Dimension.L; + int currLocLeft = getLocation(isA, Position.LEFT); + if (currLocLeft == LOC_UNKNOWN) { + setDimension(isA, dim); + setLeft(isA, locLeft); + setRight(isA, locRight); + setOn(isA, locLine); + } + else if (currLocLeft == locLeft) { + //-- nothing to do + } + else { + throw new IllegalArgumentException("Merging incompatible locations"); + } + } + + private void setDimension(boolean isA, int dimension) { + if (isA) { + aDim = dimension; + } + else { + bDim = dimension; + } + } + + 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 getLocation(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; + } + + 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(); + switch(dimension(isA)) { + case Dimension.A: + buf.append(Location.toLocationSymbol(getLocation(isA, Position.LEFT))); + buf.append(Location.toLocationSymbol(getLocation(isA, Position.ON))); + buf.append(Location.toLocationSymbol(getLocation(isA, Position.RIGHT))); + break; + case Dimension.L: + buf.append(Location.toLocationSymbol(getLocation(isA, Position.ON))); + break; + default: + buf.append("-"); + } + return buf.toString(); + } + + private int dimension(boolean isA) { + return isA ? aDim : bDim; + } + + public boolean isKnown(boolean isA) { + if (isA) return isKnown(aLocLine); + return isKnown(bLocLine); + } + + 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; + } + } + + +} 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..ee78972b42 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateGeometry.java @@ -0,0 +1,293 @@ +/* + * 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.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.locationtech.jts.algorithm.BoundaryNodeRule; +import org.locationtech.jts.algorithm.Orientation; +import org.locationtech.jts.algorithm.PointLocator; +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.CoordinateArrays; +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.Location; +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.LinearComponentExtracter; +import org.locationtech.jts.noding.BasicSegmentString; +import org.locationtech.jts.noding.SegmentString; + +public class RelateGeometry { + + public static final boolean GEOM_A = true; + public static final boolean GEOM_B = false; + + private Geometry geom; + private int dim; + private LinearBoundary lineBoundary; + private PointOnGeometryLocator areaLocator = null; + private PointLocator lineLocator = null; + private boolean isPrepared = false; + private List pts; + private Set uniquePoints; + private BoundaryNodeRule boundaryNodeRule; + + public RelateGeometry(Geometry input) { + this(input, false, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE); + } + + public RelateGeometry(Geometry input, boolean isPrepared) { + 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(); + + if (dim == 1) { + lineBoundary = new LinearBoundary(input, boundaryNodeRule); + } + } + + + public Envelope getEnvelope() { + return geom.getEnvelopeInternal(); + } + + public int getDimension() { + return dim; + } + + public int getEffectiveDimension() { + if (geom.isEmpty()) return Dimension.FALSE; + if (getDimension() == 1 && geom.getLength() == 0) + return Dimension.P; + //TODO: for mixed-dim collections, return largest non-empty dim + return geom.getDimension(); + } + + public int getDimensionBoundary() { + if (geom.isEmpty()) return Dimension.FALSE; + if (getDimension() == 1) { + if (geom.getLength() == 0) { + return Dimension.FALSE; + } + if (! lineBoundary.hasBoundary()) + return Dimension.FALSE; + } + return getDimension() - 1; + } + + public boolean isDimension(int dimension) { + return dim == dimension; + } + + public Geometry getGeometry() { + return geom; + } + + public boolean hasEdges() { + return getEffectiveDimension() > Dimension.P; + } + + public boolean isZeroLength() { + //TODO: evaluate component-wise and short-circuit + return geom.getLength() <= 0; + } + + public int locateNode(Coordinate pt) { + switch (dim) { + case Dimension.P: + return Location.INTERIOR; + case Dimension.L: + return lineBoundary.isBoundary(pt) ? Location.BOUNDARY : Location.INTERIOR; + case Dimension.A: + return Location.BOUNDARY; + } + // Assert: should never reach here + return Location.NONE; + } + + public int locate(Coordinate pt) { + //TODO: to support mixed GCs all dimensions will have to be tested + switch (dim) { + case Dimension.A: + return locateOnArea(pt); + case Dimension.L: + return locateOnLine(pt); + case Dimension.P: + return LocateOnPoint(pt); + } + + // Assert: should never reach here + return Location.NONE; + } + + private int LocateOnPoint(Coordinate pt) { + if (getUniquePoints().contains(pt)) + return Location.INTERIOR; + return Location.EXTERIOR; + } + + private int locateOnLine(Coordinate pt) { + if (lineLocator == null) { + //TODO: index the lines in prepared mode? + lineLocator = new PointLocator(boundaryNodeRule); + } + return lineLocator.locate(pt, geom); + } + + public int locateOnArea(Coordinate pt) { + //TODO: only create index after N queries + if (areaLocator == null) { + if (isPrepared) { + areaLocator = new IndexedPointInAreaLocator(geom); + } + else { + areaLocator = new SimplePointInAreaLocator(geom); + } + } + return areaLocator.locate(pt); + } + + private Set createUniquePoints() { + //TODO: make more efficient (ie by scanning geometry?) + List pts = getCoordinates(); + Set set = new HashSet(); + set.addAll(pts); + return set; + } + + public Set getUniquePoints() { + if (uniquePoints == null) { + uniquePoints = createUniquePoints(); + } + return uniquePoints; + } + + public List getCoordinates() { + if (pts == null) { + pts = ComponentCoordinateExtracter.getCoordinates(geom); + } + return pts; + } + + public List extractSegmentStringsOLD(Envelope env) { + List lines = LinearComponentExtracter.getLines(geom); + List segStrings = new ArrayList(); + for (LineString line : lines) { + if (env == null || env.intersects(line.getEnvelopeInternal())) { + SegmentString ss = new BasicSegmentString(line.getCoordinates(), null); + segStrings.add(ss); + } + } + return segStrings; + } + + public List extractSegmentStrings(Envelope env) { + List segStrings = new ArrayList(); + for (int i = 0; i < geom.getNumGeometries(); i++) { + Geometry comp = geom.getGeometryN(i); + extractSegmentStrings(comp, env, segStrings); + } + return segStrings; + } + + private void extractSegmentStrings(Geometry geom, Envelope env, List segStrings) { + if (geom.isEmpty()) + return; + boolean hasSegmentsInEnv = env == null || env.intersects(geom.getEnvelopeInternal()); + if (! hasSegmentsInEnv) + return; + + if (geom instanceof LineString) { + SegmentString ss = createSegmentString(geom.getCoordinates()); + segStrings.add(ss); + } + else if (geom instanceof Polygon) { + Polygon poly = (Polygon) geom; + extractRingToSegmentString(poly.getExteriorRing(), true, env, segStrings); + for (int i = 0; i < poly.getNumInteriorRing(); i++) { + extractRingToSegmentString(poly.getInteriorRingN(i), false, env, segStrings); + } + } + } + + private void extractRingToSegmentString(LinearRing ring, boolean requireCW, Envelope env, + List segStrings) { + if (ring.isEmpty()) + return; + if (env != null && ! env.intersects(ring.getEnvelopeInternal())) + return; + + Coordinate[] pts = ring.getCoordinates(); + boolean isFlipped = requireCW == Orientation.isCCW(pts); + if (isFlipped) { + pts = pts.clone(); + CoordinateArrays.reverse(pts); + } + SegmentString ss = createSegmentString(pts); + segStrings.add(ss); + } + + private SegmentString createSegmentString(Coordinate[] pts) { + SegmentString ss = new BasicSegmentString(pts, this); + return ss; + } + + public boolean isPointsOrPolygons() { + return geom instanceof Point + || geom instanceof MultiPoint + || geom instanceof Polygon + || geom instanceof MultiPolygon; + } + + public boolean hasAnyPoints() { + return getDimension() == Dimension.P; + } + + public Set getLineEnds() { + return lineBoundary.getEndPoints(); + } + + public int locateLineEnd(Coordinate p) { + return lineBoundary.isBoundary(p) ? Location.BOUNDARY : Location.INTERIOR; + } + + public boolean isEmpty() { + return geom.isEmpty(); + } + + public boolean hasBoundary() { + return lineBoundary.hasBoundary(); + } + +} 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..114a833918 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java @@ -0,0 +1,314 @@ +/* + * 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 static org.locationtech.jts.operation.relateng.RelateGeometry.GEOM_A; +import static org.locationtech.jts.operation.relateng.RelateGeometry.GEOM_B; + +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.IntersectionMatrix; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.geom.prep.PreparedGeometry; +import org.locationtech.jts.noding.SegmentString; +import org.locationtech.jts.operation.relate.RelateOp; + +/** + * Computes the value of topological predicates based on the DE-9IM model. + * + * The algorithm used provides the following: + *
    + *
  1. Efficient short-circuited evaluation of all predicates + *
  2. Optimized evaluation of repeated predicates against a single geometry + * (via cached spatial indexes) + *
  3. Robust computation (since only point-local topology is required) + *
  4. FUTURE Support for {@link BoundaryNodeRule} + *
  5. FUTURE Support for all GeometryCollection inputs, using union semantics + *
  6. FUTURE Support for a distance tolerance to compute approximate predicates + *
+ * + * This implementation replaces both {@link RelateOp} and {@link PreparedGeometry}. + * + * @author Martin Davis + * + * @see RelateOp + * @see PreparedGeometry + */ +public class RelateNG +{ + + public static boolean evaluate(TopologyPredicate pred, Geometry a, Geometry b) { + RelateNG rng = new RelateNG(a); + return rng.evaluate(pred, b); + } + + private static boolean evaluate(RelatePredicate pred, Geometry a, Geometry b, BoundaryNodeRule bnRule) { + RelateNG rng = new RelateNG(a, bnRule); + return rng.evaluate(pred, b); + } + + public static boolean relate(Geometry a, Geometry b, String mask) { + RelatePredicate rel = new RelatePredicate(mask); + return RelateNG.evaluate(rel, a, b); + } + + public static IntersectionMatrix relate(Geometry a, Geometry b) { + RelatePredicate rel = new RelatePredicate(); + RelateNG.evaluate(rel, a, b); + return rel.getIM(); + } + + public static IntersectionMatrix relate(Geometry a, Geometry b, BoundaryNodeRule bnRule) { + RelatePredicate rel = new RelatePredicate(); + RelateNG.evaluate(rel, a, b, bnRule); + return rel.getIM(); + } + + + private RelateGeometry geomA; + private boolean isPrepared = false; + + private EdgeSetMutualIntersector edgeMutualInt; + private BoundaryNodeRule boundaryNodeRule; + + public RelateNG(Geometry inputA) { + this(inputA, false, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE); + } + + public RelateNG(Geometry inputA, BoundaryNodeRule bnRule) { + this(inputA, false, bnRule); + } + + public RelateNG(Geometry inputA, boolean isPrepared) { + this(inputA, isPrepared, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE); + } + + public RelateNG(Geometry inputA, boolean isPrepared, BoundaryNodeRule bnRule) { + this.boundaryNodeRule = bnRule; + geomA = new RelateGeometry(inputA, isPrepared, boundaryNodeRule); + this.boundaryNodeRule = bnRule; + } + + public boolean evaluate(TopologyPredicate predicate, Geometry inputB) { + + RelateGeometry geomB = new RelateGeometry(inputB, boundaryNodeRule); + int dimA = geomA.getDimension(); + int dimB = geomB.getDimension(); + + //-- check if predicate is determined by dimension + int dimValue = predicate.valueDimensions(dimA, dimB); + if (TopologyPredicateValue.isKnown(dimValue)) + return TopologyPredicateValue.toBoolean(dimValue); + + //-- check if predicate is determined by envelopes + int envValue = predicate.valueEnvelopes(geomA.getEnvelope(), inputB.getEnvelopeInternal()); + if (TopologyPredicateValue.isKnown(envValue)) + return TopologyPredicateValue.toBoolean(envValue); + + TopologyBuilder topoBuilder = new TopologyBuilder(predicate, geomA, geomB); + + //-- optimize P/P evaluation + if (dimA == Dimension.P && dimB == Dimension.P) { + computePointPointOpt(geomB, topoBuilder); + topoBuilder.finish(); + return topoBuilder.getResult(); + } + + computeAtPoints(geomA, GEOM_A, geomB, topoBuilder); + if (topoBuilder.isResultKnown()) { + return topoBuilder.getResult(); + } + computeAtPoints(geomB, GEOM_B, geomA, topoBuilder); + if (topoBuilder.isResultKnown()) { + return topoBuilder.getResult(); + } + + if (geomA.hasEdges() && geomB.hasEdges()) { + computeEdges(geomB, topoBuilder); + } + + //-- after all processing, set remaining unknown values in IM + topoBuilder.finish(); + return topoBuilder.getResult(); + } + + /** + * An optimized algorithm for evaluating P/P cases. + * It only tests one point set against the other. + * + * @param geomB + * @param topoBuilder + */ + private void computePointPointOpt(RelateGeometry geomB, TopologyBuilder topoBuilder) { + 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++; + topoBuilder.addPointOnPointInterior(ptB); + } + else { + topoBuilder.addPointOnPointExterior(GEOM_B, ptB); + } + if (topoBuilder.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? + topoBuilder.addPointOnPointExterior(GEOM_A, null); + } + } + + private void computeAtPoints(RelateGeometry geomSrc, boolean isA, + RelateGeometry geomTarget, TopologyBuilder topoBuilder) { + + boolean isResultKnown = false; + isResultKnown = computePoints(geomSrc, isA, geomTarget, topoBuilder); + if (isResultKnown) + return; + + isResultKnown = computeLineEnds(geomSrc, isA, geomTarget, topoBuilder); + if (isResultKnown) + return; + + computeAreaNodes(geomSrc, isA, geomTarget, topoBuilder); + } + + private boolean computePoints(RelateGeometry geom, boolean isA, RelateGeometry geomTarget, + TopologyBuilder topoBuilder) { + //TODO: handle mixed GCs + if (geom.getDimension() != Dimension.P) + return false; + //TODO: get Points only (i.e. from mixed GCs) + List coords = geom.getCoordinates(); + for (Coordinate p : coords) { + //TODO: break when all possible topo locations have been found + int locOnLine = geomTarget.locate(p); + topoBuilder.addPointOnGeometry(isA, locOnLine, p); + if (topoBuilder.isResultKnown()) { + return true; + } + } + return false; + } + + private boolean computeLineEnds(RelateGeometry geom, boolean isA, RelateGeometry geomTarget, + TopologyBuilder topoBuilder) { + //TODO: handle mixed GCs + if (geom.getDimension() != Dimension.L) + return false; + + Set coords = geom.getLineEnds(); + for (Coordinate p : coords) { + //TODO: break when all possible locations have been found? + int locLineEnd = geom.locateLineEnd(p); + int locTarget = geomTarget.locate(p); + topoBuilder.addLineEndOnGeometry(isA, locLineEnd, locTarget, p); + if (topoBuilder.isResultKnown()) { + return true; + } + } + return false; + } + + private boolean computeAreaNodes(RelateGeometry geom, boolean isAreaA, RelateGeometry geomTarget, TopologyBuilder topoBuilder) { + //-- evaluate against line and area elements only + //TODO: handle mixed GCs + if (geomTarget.getDimension() < Dimension.L) + return false; + + //TODO: explore ways to avoid testing every ring + for (int i = 0; i < geom.getGeometry().getNumGeometries(); i++) { + Geometry elem = geom.getGeometry().getGeometryN(i); + if (elem.isEmpty()) + continue; + + if (elem instanceof Polygon) { + Polygon poly = (Polygon) elem; + computeAreaNode(poly.getExteriorRing(), isAreaA, geomTarget, topoBuilder); + if (topoBuilder.isResultKnown()) { + return true; + } + for (int j = 0; j < poly.getNumInteriorRing(); j++) { + computeAreaNode(poly.getInteriorRingN(j), isAreaA, geomTarget, topoBuilder); + if (topoBuilder.isResultKnown()) { + return true; + } + } + } + } + return false; + } + + private void computeAreaNode(LinearRing ring, boolean isAreaA, RelateGeometry geomTarget, TopologyBuilder topoBuilder) { + Coordinate pt = ring.getCoordinate(); + int locOnTarget = geomTarget.locate(pt); + //-- don't need to test against points, since they have already been tested against the area + topoBuilder.addAreaVertexOnLineArea(isAreaA, locOnTarget, pt); + } + + private void computeEdges(RelateGeometry geomB, TopologyBuilder topoBuilder) { + Envelope envInt = geomA.getEnvelope().intersection(geomB.getEnvelope()); + if (envInt.isNull()) + return; + + List edgesB = geomB.extractSegmentStrings(envInt); + EdgeSegmentIntersector intersector = new EdgeSegmentIntersector(topoBuilder); + + if (topoBuilder.isSelfNodingRequired()) { + computeEdgesAll(edgesB, envInt, intersector); + } + else { + computeEdgesMutual(edgesB, envInt, intersector); + } + if (topoBuilder.isResultKnown()) { + return; + } + + topoBuilder.evaluateNodes(); + } + + private void computeEdgesAll(List edgesB, Envelope envInt, EdgeSegmentIntersector intersector) { + List edgesA = geomA.extractSegmentStrings(envInt); + EdgeSetIntersector edgeInt = new EdgeSetIntersector(edgesA, edgesB, envInt); + edgeInt.process(intersector); + } + + private void computeEdgesMutual(List edgesB, Envelope envInt, EdgeSegmentIntersector intersector) { + //-- for prepared evaluation the A edge index is reused + if (edgeMutualInt == null) { + Envelope envTarget = isPrepared ? null : envInt; + List edgesA = geomA.extractSegmentStrings(envTarget); + edgeMutualInt = new EdgeSetMutualIntersector(edgesA, envTarget); + } + + 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..e881a144ef --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNode.java @@ -0,0 +1,165 @@ +/* + * 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; + +class RelateNode { + + private Coordinate nodePt; + private ArrayList edges = new ArrayList(); + + public RelateNode(Coordinate pt) { + this.nodePt = pt; + } + + public Coordinate getCoordinate() { + return nodePt; + } + + public List getEdges() { + return edges; + } + + public void addAreaEdge(Coordinate p, boolean isA, boolean isForward) { + if (isForward) { + addEdge(p, isA, Location.EXTERIOR, Location.INTERIOR, Location.BOUNDARY); + } + else { + addEdge(p, isA, Location.INTERIOR, Location.EXTERIOR, Location.BOUNDARY); + } + } + + public void addLineEdge(Coordinate p, boolean isA) { + addEdge(p, isA, Location.EXTERIOR, Location.EXTERIOR, Location.INTERIOR); + } + + private void addEdge(Coordinate pt, boolean isA, int locLeft, int locRight, int locLine) { + int insertIndex = -1; + for (int i = 0; i < edges.size(); i++) { + RelateEdge e = edges.get(i); + int comp = e.compareToEdge(pt); + if (comp == 0) { + e.merge(pt, isA, locLeft, locRight, locLine); + return; + } + if (comp == -1 ) { + continue; + } + if (comp == 1 ) { + insertIndex = i; + break; + } + } + //-- add a new edge + RelateEdge e = createEdge(pt, isA, locLeft, locRight, locLine); + if (insertIndex < 0) { + edges.add(e); + } + else { + edges.add(insertIndex, e); + } + } + + private RelateEdge createEdge(Coordinate pt, boolean isA, int locLeft, int locRight, int locLine) { + if (locLine == Location.BOUNDARY) + return new RelateEdge(this, pt, isA, locLeft, locRight); + //-- create line edge + return new RelateEdge(this, pt, isA, locLine); + } + + /** + * Computes the final topology for the edges around this node. + */ + public void finishTopology(int dimA, int dimB) { + finishNode(true, dimA); + finishNode(false, dimB); + } + + private void finishNode(boolean isA, int dim) { + if (dim == Dimension.L) { + finishLine(isA); + } + else { + finishArea(isA); + } + } + + private void finishLine(boolean isA) { + for (RelateEdge e : edges) { + if (! e.isKnown(isA)) { + e.setDimLocations(isA, Dimension.L, Location.EXTERIOR); + } + } + } + + private void finishArea(boolean isA) { + int startIndex = findKnownEdgeIndex(edges, isA); + if (startIndex == -1) { + //-- no edges for this geometry + setDimLocations(isA, Dimension.A, Location.EXTERIOR); + return; + } + int currLoc = edges.get(startIndex).getLocation(isA, Position.LEFT); + //-- edges are stored in CCW order + int index = nextIndex(edges, startIndex); + while (index != startIndex) { + RelateEdge e = edges.get(index); + if (e.isKnown(isA)) { + //TODO: assert loc(RIGHT) == currLoc + currLoc = e.getLocation(isA, Position.LEFT); + } + else { //-- not known + e.setDimLocations(isA, Dimension.A, currLoc); + } + index = nextIndex(edges, index); + } + } + + private void setDimLocations(boolean isA, int dim, int loc) { + for (RelateEdge e : edges) { + e.setDimLocations(isA, dim, loc); + } + } + + private static int nextIndex(List list, int i) { + if (i >= list.size() - 1) { + return 0; + } + return i + 1; + } + + private 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 String toString() { + StringBuilder buf = new StringBuilder(); + for (RelateEdge e : edges) { + buf.append(e.toString()); + buf.append("\n"); + } + return buf.toString(); + } + +} 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..8bc2fb0d83 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePredicate.java @@ -0,0 +1,38 @@ +package org.locationtech.jts.operation.relateng; + +class RelatePredicate extends IMPredicate +{ + private String mask = null; + + public RelatePredicate() { + } + + public RelatePredicate(String mask) { + this.mask = mask; + } + + public String name() { return "relate"; } + + @Override + public int valuePartial(int dimA, int dimB) { + return TopologyPredicateValue.UNKNOWN; + } + + @Override + public boolean value(int dimA, int dimB) { + if (mask == null) + return false; + + boolean val = intMatrix.matches(mask); + if (! val) { + System.out.println( intMatrix + " does not equal mask " + mask); + } + return val; + } + + public String toString() { + if (mask != null) + return name() + "(" + mask + ")"; + return name(); + } +} \ No newline at end of file diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/SimplePredicate.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/SimplePredicate.java new file mode 100644 index 0000000000..062d4cdacf --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/SimplePredicate.java @@ -0,0 +1,52 @@ +package org.locationtech.jts.operation.relateng; + +import org.locationtech.jts.geom.Location; + +public abstract class SimplePredicate implements TopologyPredicate { + + private int value = TopologyPredicateValue.UNKNOWN; + + public boolean isSelfNodingRequired() { + return false; + } + + /** + * Updates the predicate value to the given state + * if it is currently unknown. + * + * @param val the predicate value to update + */ + protected void updateValue(boolean val) { + //-- don't change already-known value + if (isKnown()) + return; + value = TopologyPredicateValue.toValue(val); + } + + @Override + public int valuePartial(int dimA, int dimB) { + return value; + } + + @Override + public boolean value(int dimA, int dimB) { + return TopologyPredicateValue.toBoolean(value); + } + + public boolean isKnown() { + return TopologyPredicateValue.isKnown(value); + } + + /** + * 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; + } +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyBuilder.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyBuilder.java new file mode 100644 index 0000000000..5f43c25ea1 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyBuilder.java @@ -0,0 +1,431 @@ +/* + * 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; + +public class TopologyBuilder { + + private static final String MSG_GEOMETRY_DIMENSION_UNEXPECTED = "Unexpected combination of geometry dimensions"; + + private TopologyPredicate predicate; + private RelateGeometry geomA; + private RelateGeometry geomB; + private int dimA; + private int dimB; + private int predicateValue; + private Map nodeMap = new HashMap(); + + public TopologyBuilder(TopologyPredicate predicate, RelateGeometry geomA, RelateGeometry geomB) { + this.predicate = predicate; + this.geomA = geomA; + this.geomB = geomB; + this.dimA = geomA.getDimension(); + this.dimB = geomB.getDimension(); + + initExteriorDims(); + predicateValue = predicate.valueDimensions(dimA, dimB); + } + + /** + * Determine some topology in the EXTERIORs a priori. + */ + private void initExteriorDims() { + int dimAEff = geomA.getEffectiveDimension(); + //int dimABdyEff = geomA.getEffectiveDimension(); + int dimBEff = geomB.getEffectiveDimension(); + /** + * For P/A case, the Area Int and Bdy intersect the Point exterior. + */ + 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.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); + } + 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 = dim(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.INTERIOR, Location.EXTERIOR, Dimension.A); + updateDim(geomNonEmpty, Location.BOUNDARY, Location.EXTERIOR, Dimension.L); + break; + } + + } + + private int dim(boolean isA) { + return isA ? dimA : dimB; + } + + private RelateGeometry getGeometry(boolean isA) { + return isA ? geomA : geomB; + } + + public boolean isA(RelateGeometry geom) { + return geom == geomA; + } + + public boolean isAreaArea() { + return dimA == Dimension.A && dimB == Dimension.A; + } + + public boolean isSelfNodingRequired() { + if (geomA.isPointsOrPolygons()) return false; + if (geomB.isPointsOrPolygons()) return false; + return predicate.isSelfNodingRequired(); + } + + /** + * + * @param locA + * @param locB + * @param dimension + * @return true if the value of the predicate changed + */ + private boolean updateDim(int locA, int locB, int dimension) { + predicate.updateDim(locA, locB, dimension); + predicateValue = predicate.valuePartial(dimA, dimB); + return true; + } + + private boolean updateDim(boolean isAB, int loc1, int loc2, int dimension) { + if (isAB) { + return updateDim(loc1, loc2, dimension); + } + // is ordered BA + return updateDim(loc2, loc1, dimension); + } + + public boolean isResultKnown() { + return TopologyPredicateValue.isKnown(predicateValue); + } + + public boolean getResult() { + return TopologyPredicateValue.toBoolean(predicateValue); + } + + /** + * Finalizes the matrix by setting UNKNOWN values to Dimension.FALSE (empty). + */ + public void finish() { + predicate.finish(); + predicateValue = TopologyPredicateValue.toValue(predicate.value(dimA, dimB)); + } + + private RelateNode getNode(Coordinate intPt) { + RelateNode node = nodeMap.get(intPt); + if (node == null) { + node = new RelateNode(intPt); + nodeMap.put(intPt, node); + } + return node; + } + + public void addIntersectionProper(Coordinate a0, Coordinate a1, Coordinate b0, Coordinate b1, Coordinate intPt) { + if (dimA == 2 && dimB == 2) { + //- a proper edge intersection is an edge cross. + addAreaAreaIntersection(true, a0, a1, b0, b1, intPt); + } + else if (dimA == 2 && dimB == 1) { + addAreaLineCross(RelateGeometry.GEOM_A, a0, a1, b0, b1, intPt); + } + else if (dimA == 1 && dimB == 2) { + addAreaLineCross(RelateGeometry.GEOM_B, a0, a1, b0, b1, intPt); + } + else if (dimA == 1 && dimB == 1) { + addLineLineCross(a0, a1, b0, b1, intPt); + } + else { + Assert.shouldNeverReachHere(MSG_GEOMETRY_DIMENSION_UNEXPECTED); + } + } + + public void addSelfIntersectionProper(boolean isA, + Coordinate p00, Coordinate p01, + Coordinate p10, Coordinate p11, + Coordinate intPt) { + //-- create a node at the crossing + addEdge(isA, intPt, p00, RelateEdge.IS_REVERSE); + addEdge(isA, intPt, p01, RelateEdge.IS_FORWARD); + addEdge(isA, intPt, p10, RelateEdge.IS_REVERSE); + addEdge(isA, intPt, p11, RelateEdge.IS_FORWARD); + } + + private void addLineLineCross( + Coordinate a0, Coordinate a1, + Coordinate b0, Coordinate b1, + Coordinate intPt) { + //-- create a node at the crossing + addEdgeIntersectionNode(intPt); + + addEdge(RelateGeometry.GEOM_A, intPt, a0, RelateEdge.IS_REVERSE); + addEdge(RelateGeometry.GEOM_A, intPt, a1, RelateEdge.IS_FORWARD); + addEdge(RelateGeometry.GEOM_B, intPt, b0, RelateEdge.IS_REVERSE); + addEdge(RelateGeometry.GEOM_B, intPt, b1, RelateEdge.IS_FORWARD); + } + + private void addAreaLineCross(boolean geomArea, Coordinate a0, Coordinate a1, Coordinate b0, Coordinate b1, Coordinate intPt) { + /** + * 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 computed. + */ + int locLine = getGeometry(! geomArea).locateNode(intPt); + updateDim(geomArea, Location.BOUNDARY, locLine, Dimension.P); + + //-- create a node to allow full topology computation + addEdge(RelateGeometry.GEOM_A, intPt, a0, RelateEdge.IS_REVERSE); + addEdge(RelateGeometry.GEOM_A, intPt, a1, RelateEdge.IS_FORWARD); + addEdge(RelateGeometry.GEOM_B, intPt, b0, RelateEdge.IS_REVERSE); + addEdge(RelateGeometry.GEOM_B, intPt, b1, RelateEdge.IS_FORWARD); + } + + public void addAreaAreaIntersection(boolean isProper, Coordinate a0, Coordinate a2, Coordinate b0, + Coordinate b2, Coordinate intPt) { + //-- record point location at edge intersection + updateDim(Location.BOUNDARY, Location.BOUNDARY, Dimension.P); + + /** + * A crossing of area edges provides limited topological information, + * since the edge intersection point + * may also be a node of a hole, or of another shell, or both. + * So the edges on either side of the node may be in the area interiors or exteriors, + * and thus it is not possible to infer the edges location. + * Full topology is determined when the node topology is evaluated. + */ + if (isProper || PolygonNodeTopology.isCrossing(intPt, a0, a2, b0, b2)) { + updateDim(Location.INTERIOR, Location.INTERIOR, Dimension.A); + updateDim(Location.BOUNDARY, Location.BOUNDARY, Dimension.P); + } + + /** + * Add edges to node topology. + * Locations are based on CW ring orientation (interior to right). + * a0/b0 lie before the node and a2/b2 are after, so directions are opposite. + */ + RelateNode node = getNode(intPt); + node.addAreaEdge(a0, RelateGeometry.GEOM_A, RelateEdge.IS_REVERSE); + node.addAreaEdge(a2, RelateGeometry.GEOM_A, RelateEdge.IS_FORWARD); + + node.addAreaEdge(b0, RelateGeometry.GEOM_B, RelateEdge.IS_REVERSE); + node.addAreaEdge(b2, RelateGeometry.GEOM_B, RelateEdge.IS_FORWARD); + //System.out.println(node); + } + + public void addIntersectionCollinear(Coordinate a0, Coordinate a1, Coordinate b0, Coordinate b1, Coordinate int0, Coordinate int1) { + addEdgesAtNode(RelateGeometry.GEOM_A, a0, a1, int0); + addEdgesAtNode(RelateGeometry.GEOM_A, a0, a1, int1); + addEdgesAtNode(RelateGeometry.GEOM_B, b0, b1, int0); + addEdgesAtNode(RelateGeometry.GEOM_B, b0, b1, int1); + } + + public void addSelfIntersectionCollinear(boolean isA, Coordinate a0, Coordinate a1, Coordinate b0, Coordinate b1, Coordinate int0, Coordinate int1) { + addEdgesAtNode(isA, a0, a1, int0); + addEdgesAtNode(isA, a0, a1, int1); + addEdgesAtNode(isA, b0, b1, int0); + addEdgesAtNode(isA, b0, b1, int1); + } + + private void addEdgesAtNode(boolean isA, Coordinate p0, Coordinate p1, Coordinate intPt) { + if (! intPt.equals2D(p1)) { + addEdge(isA, intPt, p1, RelateEdge.IS_FORWARD); + } + if (! intPt.equals2D(p0)) { + addEdge(isA, intPt, p0, RelateEdge.IS_REVERSE); + } + } + + /** + * Adds a basic edge intersection point. + * @param locB + * @param locA + * + * @param pt + */ + public void addEdgeIntersectionNode(Coordinate pt) { + int locA = geomA.locateNode(pt); + int locB = geomB.locateNode(pt); + 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 isGeomA, int loc, Coordinate pt) { + updateDim(isGeomA, Location.INTERIOR, loc, Dimension.P); + } + + public void addLineEndOnGeometry(boolean isLineA, int locLineEnd, int locTarget, Coordinate pt) { + int dimOther = dim(! isLineA); + if (dimOther == Dimension.P) { + addLineEndOnPoint(isLineA, locLineEnd, locTarget, pt); + } + else if (dimOther == Dimension.L) { + addLineEndOnLine(isLineA, locLineEnd, locTarget, pt); + } + else if (dimOther == Dimension.A) { + addLineEndOnArea(isLineA, locLineEnd, locTarget, pt); + } + } + + private void addLineEndOnPoint(boolean isLineA, int locLineEnd, int locPoint, Coordinate pt) { + updateDim(isLineA, locLineEnd, locPoint, Dimension.P); + } + + 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); + } + } + + private void addLineEndOnLine(boolean isGeomA, int locLineEnd, int locLine, Coordinate pt) { + updateDim(isGeomA, 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(isGeomA, Location.INTERIOR, Location.EXTERIOR, Dimension.L); + } + } + + public void addAreaVertexOnLineArea(boolean isGeomA, int loc, Coordinate pt) { + if (dim(! isGeomA) == Dimension.L) { + addAreaVertexOnLine(isGeomA, loc, pt); + } + else { + addAreaVertexOnArea(isGeomA, loc, pt); + } + } + + private void addAreaVertexOnLine(boolean isGeomA, int loc, Coordinate pt) { + if (loc == Location.EXTERIOR) { + updateDim(isGeomA, Location.INTERIOR, Location.EXTERIOR, Dimension.A); + updateDim(isGeomA, Location.BOUNDARY, Location.EXTERIOR, Dimension.L); + updateDim(isGeomA, Location.EXTERIOR, Location.EXTERIOR, Dimension.A); + } + else { + updateDim(isGeomA, Location.BOUNDARY, loc, Dimension.P); + } + } + + public void addAreaVertexOnArea(boolean isGeomA, int loc, Coordinate pt) { + if (loc == Location.BOUNDARY) { + updateDim(isGeomA, Location.BOUNDARY, Location.BOUNDARY, Dimension.P); + } + else { + updateDim(isGeomA, Location.INTERIOR, loc, Dimension.A); + updateDim(isGeomA, Location.BOUNDARY, loc, Dimension.L); + updateDim(isGeomA, Location.EXTERIOR, loc, Dimension.A); + } + } + + public void evaluateNodes() { + for (RelateNode node : nodeMap.values()) { + node.finishTopology(dimA, dimB); +//System.out.println("evaluating node: " + node); // DEBUG! + evaluateNode(node); + if (isResultKnown()) + return; + } + } + + private void evaluateNode(RelateNode node) { + for (RelateEdge e : node.getEdges()) { + if (isAreaArea()) { + updateDim(e.getLocation(RelateGeometry.GEOM_A, Position.LEFT), + e.getLocation(RelateGeometry.GEOM_B, Position.LEFT), Dimension.A); + updateDim(e.getLocation(RelateGeometry.GEOM_A, Position.RIGHT), + e.getLocation(RelateGeometry.GEOM_B, Position.RIGHT), Dimension.A); + } + updateDim(e.getLocation(RelateGeometry.GEOM_A, Position.ON), + e.getLocation(RelateGeometry.GEOM_B, Position.ON), Dimension.L); + } + } + + public void addEdge(boolean isA, Coordinate nodePt, Coordinate dirPt, boolean isForward) { + //-- vertices may be null if intersection is at end of a line + if (nodePt == null || dirPt == null) + return; + //-- skip edges where roundoff error has made them zero-length + if (nodePt.equals2D(dirPt)) + return; + + //Assert: nodePt != p + RelateNode node = getNode(nodePt); + if (dim(isA) == Dimension.A) { + node.addAreaEdge(dirPt, isA, isForward); + } + else { + node.addLineEdge(dirPt, isA); + } + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicate.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicate.java new file mode 100644 index 0000000000..b2ac3a730c --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicate.java @@ -0,0 +1,40 @@ +/* + * 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; + +public interface TopologyPredicate { + + String name(); + + default boolean isSelfNodingRequired() { + return true; + } + + void updateDim(int locA, int locB, int dimension); + + default int valueDimensions(int dimA, int dimB) { + return TopologyPredicateValue.UNKNOWN; + } + + default int valueEnvelopes(Envelope envA, Envelope envB) { + return TopologyPredicateValue.UNKNOWN; + } + + int valuePartial(int dimA, int dimB); + + void finish(); + + boolean value(int dimA, int dimB); + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateFactory.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateFactory.java new file mode 100644 index 0000000000..d76b869ec3 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateFactory.java @@ -0,0 +1,347 @@ +package org.locationtech.jts.operation.relateng; + +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Location; + +public class TopologyPredicateFactory { + + public static TopologyPredicate intersects() { + return new SimplePredicate() { + + public String name() { return "intersects"; } + + @Override + public void updateDim(int locA, int locB, int dimension) { + if (isIntersection(locA, locB)) { + updateValue(true); + } + } + + @Override + public int valueEnvelopes(Envelope envA, Envelope envB) { + return requireIntersects(envA, envB); + } + + @Override + public void finish() { + updateValue(false); + } + + }; + } + + public static TopologyPredicate disjoint() { + return new SimplePredicate() { + + public String name() { return "disjoint"; } + + @Override + public void updateDim(int locA, int locB, int dimension) { + if (isIntersection(locA, locB)) { + updateValue(false); + } + } + + @Override + public int valueEnvelopes(Envelope envA, Envelope envB) { + return valueIf(true, envA.disjoint(envB)); + } + + @Override + public void finish() { + //-- no intersecting locations have been found + updateValue(true); + } + + }; + } + + public static TopologyPredicate contains() { + return new IMPredicate() { + + public String name() { return "contains"; } + + @Override + public int valueDimensions(int dimA, int dimB) { + return require( isDimsCompatibleWithCovers(dimA, dimB) ); + } + + @Override + public int valueEnvelopes(Envelope envA, Envelope envB) { + return requireCovers(envA, envB); + } + + @Override + public int valuePartial(int dimA, int dimB) { + return valueIf( false, intersectsExteriorOf(RelateGeometry.GEOM_A)); + } + + @Override + public boolean value(int dimA, int dimB) { + return intMatrix.isContains(); + } + }; + } + + public static TopologyPredicate within() { + return new IMPredicate() { + + public String name() { return "within"; } + + @Override + public int valueDimensions(int dimA, int dimB) { + return require( isDimsCompatibleWithCovers(dimB, dimA) ); + } + + @Override + public int valueEnvelopes(Envelope envA, Envelope envB) { + return requireCovers(envB, envA); + } + + @Override + public int valuePartial(int dimA, int dimB) { + return valueIf( false, intersectsExteriorOf(RelateGeometry.GEOM_B)); + } + + @Override + public boolean value(int dimA, int dimB) { + return intMatrix.isWithin(); + } + }; + } + + public static TopologyPredicate covers() { + return new IMPredicate() { + + public String name() { return "covers"; } + + @Override + public int valueDimensions(int dimA, int dimB) { + return require(isDimsCompatibleWithCovers(dimA, dimB)); + } + + @Override + public int valueEnvelopes(Envelope envA, Envelope envB) { + return requireCovers(envA, envB); + } + + @Override + public int valuePartial(int dimA, int dimB) { + return valueIf( false, intersectsExteriorOf(RelateGeometry.GEOM_A)); + } + + @Override + public boolean value(int dimA, int dimB) { + return intMatrix.isCovers(); + } + }; + } + + public static TopologyPredicate coveredBy() { + return new IMPredicate() { + public String name() { return "coveredBy"; } + + @Override + public int valueDimensions(int dimA, int dimB) { + return require(isDimsCompatibleWithCovers(dimB, dimA)); + } + + @Override + public int valueEnvelopes(Envelope envA, Envelope envB) { + return requireCovers(envB, envA); + } + + @Override + public int valuePartial(int dimA, int dimB) { + return valueIf( false, intersectsExteriorOf(RelateGeometry.GEOM_B)); + } + + @Override + public boolean value(int dimA, int dimB) { + return intMatrix.isCoveredBy(); + } + }; + } + + public static TopologyPredicate crosses() { + return new IMPredicate() { + public String name() { return "crosses"; } + + @Override + public int valueDimensions(int dimA, int dimB) { + //-- value is FALSE for P/P and A/A situations + boolean isBothPointsOrAreas = (dimA == Dimension.P && dimB == Dimension.P) + || (dimA == Dimension.A && dimB == Dimension.A); + return require(! isBothPointsOrAreas); + } + + @Override + public int valuePartial(int dimA, int dimB) { + if (dimA == Dimension.L && dimB == Dimension.L) { + //-- L/L interaction can only be dim = 0 + if (getDim(Location.INTERIOR, Location.INTERIOR) > Dimension.P) + return TopologyPredicateValue.FALSE; + } + if (dimA < dimB) { + if (isIntersects(Location.INTERIOR, Location.INTERIOR) + && isIntersects(Location.INTERIOR, Location.EXTERIOR)) { + return TopologyPredicateValue.TRUE; + } + } + else if (dimA > dimB) { + if (isIntersects(Location.INTERIOR, Location.INTERIOR) + && isIntersects(Location.EXTERIOR, Location.INTERIOR)) { + return TopologyPredicateValue.TRUE; + } + } + return TopologyPredicateValue.UNKNOWN; + } + + @Override + public boolean value(int dimA, int dimB) { + return intMatrix.isCrosses(dimA, dimB); + } + }; + } + + public static TopologyPredicate equalsTopo() { + return new IMPredicate() { + public String name() { return "equals"; } + + @Override + public int valueDimensions(int dimA, int dimB) { + return require(dimA == dimB); + } + + @Override + public int valueEnvelopes(Envelope envA, Envelope envB) { + return requireEquals(envA, envB); + } + + @Override + public int valuePartial(int dimA, int dimB) { + boolean isEitherExteriorIntersects = + isIntersects(Location.INTERIOR, Location.EXTERIOR) + || isIntersects(Location.BOUNDARY, Location.EXTERIOR) + || isIntersects(Location.EXTERIOR, Location.INTERIOR) + || isIntersects(Location.EXTERIOR, Location.BOUNDARY); + + return valueIf(false, isEitherExteriorIntersects); + } + + @Override + public boolean value(int dimA, int dimB) { + return intMatrix.isEquals(dimA, dimB); + } + }; + } + + public static TopologyPredicate overlaps() { + return new IMPredicate() { + public String name() { return "overlaps"; } + + @Override + public int valueDimensions(int dimA, int dimB) { + return require( dimA == dimB ); + } + + @Override + public int valuePartial(int dimA, int dimB) { + if (dimA == Dimension.A || dimA == Dimension.P) { + if (isIntersects(Location.INTERIOR, Location.INTERIOR) + && isIntersects(Location.INTERIOR, Location.EXTERIOR) + && isIntersects(Location.EXTERIOR, Location.INTERIOR)) + return TopologyPredicateValue.TRUE; + } + if (dimA == Dimension.L) { + if (isDim(Location.INTERIOR, Location.INTERIOR, 1) + && isIntersects(Location.INTERIOR, Location.EXTERIOR) + && isIntersects(Location.EXTERIOR, Location.INTERIOR)) + return TopologyPredicateValue.TRUE; + } + return TopologyPredicateValue.UNKNOWN; + } + + @Override + public boolean value(int dimA, int dimB) { + return intMatrix.isOverlaps(dimA, dimB); + } + }; + } + + public static TopologyPredicate touches() { + return new IMPredicate() { + public String name() { return "touches"; } + + @Override + public int valueDimensions(int dimA, int dimB) { + //-- Points have only interiors, so cannot touch + boolean isBothPoints = dimA == 0 && dimB == 0; + return require(! isBothPoints); + } + + @Override + public int valuePartial(int dimA, int dimB) { + //-- for touches interiors cannot intersect + boolean isInteriorsIntersects = isIntersects(Location.INTERIOR, Location.INTERIOR); + return valueIf(false, isInteriorsIntersects); + } + + @Override + public boolean value(int dimA, int dimB) { + return intMatrix.isTouches(dimA, dimB); + } + }; + } + + public static int requireEquals(Envelope envA, Envelope envB) { + return require(envA.equals(envB)); + } + + public static int requireCovers(Envelope envA, Envelope envB) { + return require(envA.covers(envB)); + } + + public static int requireIntersects(Envelope envA, Envelope envB) { + return require(envA.intersects(envB)); + } + + /** + * Returns FALSE if the condition is not met, + * or UNKNOWN if it is. + * Equivalent to "valueFalseIfNot". + * + * @param cond the required condition + * @return FALSE or UNKNOWN + */ + public static int require(boolean cond) { + if (! cond) { + return TopologyPredicateValue.FALSE; + } + return TopologyPredicateValue.UNKNOWN; + } + + /** + * Returns a known predicate value if the condition holds. + * Otherwise returns UNKNOWN. + * + * @param value the predicate value to return + * @param cond the condition to test + * @return the value, or UNKNOWN + */ + public static int valueIf(boolean value, boolean cond) { + if (cond) { + return TopologyPredicateValue.toValue(value); + } + return TopologyPredicateValue.UNKNOWN; + } + + 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; + } + +} 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..2cfc1acc34 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateTracer.java @@ -0,0 +1,56 @@ +package org.locationtech.jts.operation.relateng; + +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Location; + +public class TopologyPredicateTracer implements TopologyPredicate { + + private TopologyPredicate pred; + + TopologyPredicateTracer(TopologyPredicate pred) { + this.pred = pred; + } + + public String name() { return pred.name(); } + + @Override + public void updateDim(int locA, int locB, int dimension) { + System.out.println("A:" + Location.toLocationSymbol(locA) + + "/B:" + Location.toLocationSymbol(locB) + + " -> " + dimension); + pred.updateDim(locA, locB, dimension); + } + + @Override + public int valueDimensions(int dimA, int dimB) { + return pred.valueDimensions(dimA, dimB); + } + + @Override + public int valueEnvelopes(Envelope envA, Envelope envB) { + return pred.valueEnvelopes(envA, envB); + } + + @Override + public int valuePartial(int dimA, int dimB) { + int val = pred.valuePartial(dimA, dimB); + if (TopologyPredicateValue.isKnown(val)) { + System.out.println(name() + " = " + TopologyPredicateValue.toBoolean(val)); + } + return val; + } + + @Override + public void finish() { + pred.finish(); + } + + @Override + public boolean value(int dimA, int dimB) { + return pred.value(dimA, dimB); + } + + public String toString() { + return pred.toString(); + } +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateValue.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateValue.java new file mode 100644 index 0000000000..879e41a893 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateValue.java @@ -0,0 +1,22 @@ +package org.locationtech.jts.operation.relateng; + +public class TopologyPredicateValue { + + public static final int UNKNOWN = -1; + public static final int FALSE = 0; + public static final int TRUE = 1; + + public static boolean isKnown(int value) { + return value > UNKNOWN; + } + + public static boolean toBoolean(int value) { + // TODO: check for unknown value? + return value == TRUE; + } + + public static int toValue(boolean val) { + return val ? TRUE : FALSE; + } + +} 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/RelateNGTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTest.java new file mode 100644 index 0000000000..e12e640095 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTest.java @@ -0,0 +1,549 @@ +/* + * 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 junit.textui.TestRunner; +import test.jts.GeometryTestCase; + +public class RelateNGTest extends GeometryTestCase { + + 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 testLinePointZeroLength() { + String a = "LINESTRING (0 0, 0 0)"; + String b = "POINT (0 0)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkContainsWithin(b, a, true); + checkCoversCoveredBy(a, b, true); + checkCoversCoveredBy(b, a, 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); + } + + //======= 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))"; + 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 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(TopologyPredicateFactory.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(TopologyPredicateFactory.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(TopologyPredicateFactory.within(), a, b, false); + checkTouches(a, b, true); + } + + private void checkIntersectsDisjoint(String wkta, String wktb, boolean expectedValue) { + checkPredicate(TopologyPredicateFactory.intersects(), wkta, wktb, expectedValue); + checkPredicate(TopologyPredicateFactory.intersects(), wktb, wkta, expectedValue); + checkPredicate(TopologyPredicateFactory.disjoint(), wkta, wktb, ! expectedValue); + checkPredicate(TopologyPredicateFactory.disjoint(), wktb, wkta, ! expectedValue); + } + + private void checkContainsWithin(String wkta, String wktb, boolean expectedValue) { + checkPredicate(TopologyPredicateFactory.contains(), wkta, wktb, expectedValue); + checkPredicate(TopologyPredicateFactory.within(), wktb, wkta, expectedValue); + } + + private void checkCoversCoveredBy(String wkta, String wktb, boolean expectedValue) { + checkPredicate(TopologyPredicateFactory.covers(), wkta, wktb, expectedValue); + checkPredicate(TopologyPredicateFactory.coveredBy(), wktb, wkta, expectedValue); + } + + private void checkCrosses(String wkta, String wktb, boolean expectedValue) { + checkPredicate(TopologyPredicateFactory.crosses(), wkta, wktb, expectedValue); + checkPredicate(TopologyPredicateFactory.crosses(), wktb, wkta, expectedValue); + } + + private void checkOverlaps(String wkta, String wktb, boolean expectedValue) { + checkPredicate(TopologyPredicateFactory.overlaps(), wkta, wktb, expectedValue); + checkPredicate(TopologyPredicateFactory.overlaps(), wktb, wkta, expectedValue); + } + + private void checkTouches(String wkta, String wktb, boolean expectedValue) { + checkPredicate(TopologyPredicateFactory.touches(), wkta, wktb, expectedValue); + checkPredicate(TopologyPredicateFactory.touches(), wktb, wkta, expectedValue); + } + + private void checkEquals(String wkta, String wktb, boolean expectedValue) { + checkPredicate(TopologyPredicateFactory.equalsTopo(), wkta, wktb, expectedValue); + checkPredicate(TopologyPredicateFactory.equalsTopo(), wktb, wkta, expectedValue); + } + + private void checkRelate(String wktb, String wkta, String mask) { + checkPredicate(new RelatePredicate(mask), wktb, wkta, true); + } + + private void checkPredicate(TopologyPredicate pred, String wkta, String wktb, boolean expectedValue) { + Geometry a = read(wkta); + Geometry b = read(wktb); + System.out.println("Pred: " + pred.name()); + TopologyPredicate predTrace = new TopologyPredicateTracer(pred); + boolean actualVal = RelateNG.evaluate(predTrace, a, b); + assertEquals(expectedValue, actualVal); + } +} 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..66030abe0c --- /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.TopologyPredicateFactory; + +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.evaluate(TopologyPredicateFactory.intersects(), geomA, b); + } + } + + public void runIntersectsNGPrep() + { + RelateNG rng = new RelateNG(geomA, true); + for (Geometry b : geomB) { + rng.evaluate(TopologyPredicateFactory.intersects(), b); + } + } + + 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.evaluate(TopologyPredicateFactory.contains(), geomA, b); + } + } + + public void runContainsNGPrep() + { + RelateNG rng = new RelateNG(geomA, true); + for (Geometry b : geomB) { + rng.evaluate(TopologyPredicateFactory.contains(), b); + } + } + + public void xrunContainsNGPrepValidate() + { + RelateNG rng = new RelateNG(geomA, true); + for (Geometry b : geomB) { + boolean resultNG = rng.evaluate(TopologyPredicateFactory.contains(), b); + 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..317715059d --- /dev/null +++ b/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonsAdjacentPerfTest.java @@ -0,0 +1,148 @@ +/* + * 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.RelateNG; +import org.locationtech.jts.operation.relateng.TopologyPredicateFactory; + +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[] { 200000 }); + setRunIterations(N_ITER); + } + + public void setUp() throws Exception + { + String resource = "europe.wkt"; + //String resource = "world.wkt"; + loadPolygons(resource); + + System.out.println("RelateNG Adjacent Polygons Performance Test"); + System.out.println("Resource: " + resource); + + System.out.println("# geometries: " + polygons.size() + + " # pts: " + numPts(polygons)); + } + + 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.evaluate(TopologyPredicateFactory.intersects(), a, b); + } + } + } + + public void runIntersectsNGPrep() + { + for (Geometry a : polygons) { + RelateNG rng = new RelateNG(a, true); + for (Geometry b : polygons) { + rng.evaluate(TopologyPredicateFactory.intersects(), b); + } + } + } + + 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.evaluate(TopologyPredicateFactory.touches(), a, b); + } + } + } + + public void runTouchesNGPrep() + { + for (Geometry a : polygons) { + RelateNG rng = new RelateNG(a, true); + for (Geometry b : polygons) { + rng.evaluate(TopologyPredicateFactory.touches(), b); + } + } + } + +} 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..a08f44bbe1 --- /dev/null +++ b/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonsOverlappingPerfTest.java @@ -0,0 +1,181 @@ +/* + * 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.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.TopologyPredicateFactory; + +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 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 = createTestGeoms(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.evaluate(TopologyPredicateFactory.intersects(), geomA, b); + } + } + + public void runIntersectsNGPrep() + { + RelateNG rng = new RelateNG(geomA, true); + for (Geometry b : geomB) { + rng.evaluate(TopologyPredicateFactory.intersects(), b); + } + } + + 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.evaluate(TopologyPredicateFactory.contains(), geomA, b); + } + } + + public void runContainsNGPrep() + { + RelateNG rng = new RelateNG(geomA, true); + for (Geometry b : geomB) { + rng.evaluate(TopologyPredicateFactory.contains(), b); + } + } + + public void xrunContainsNGPrepValidate() + { + RelateNG rng = new RelateNG(geomA, true); + for (Geometry b : geomB) { + boolean resultNG = rng.evaluate(TopologyPredicateFactory.contains(), b); + boolean resultOld = geomA.contains(b); + assertEquals(resultNG, resultOld); + } + } + + private Geometry[] createTestGeoms(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; + } + + +}