diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 5b7d042e3f..7f85f2b29f 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -57,6 +57,7 @@ Distributions for older JTS versions can be obtained at the * Add Voronoi snapping heuristic to fix invalid diagram topology (#1174) * Fix `LineSegment.project` to handle segments projecting onto a single endpoint (#1179) * Fix DD equals and compareTo (#1186) +* Fix `RelateNG.computeLineEnds` incorrectly skipping boundary points for disjoint line components (#1175) ### Performance Improvements diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java index c0f0594069..99acb07574 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java @@ -395,7 +395,8 @@ private boolean computeLineEnds(RelateGeometry geom, boolean isA, RelateGeometry return false; } - boolean hasExteriorIntersection = false; + boolean hasInteriorExteriorIntersection = false; + boolean hasBoundaryExteriorIntersection = false; Iterator geomi = new GeometryCollectionIterator(geom.getGeometry()); while (geomi.hasNext()) { Geometry elem = (Geometry) geomi.next(); @@ -403,21 +404,26 @@ private boolean computeLineEnds(RelateGeometry geom, boolean isA, RelateGeometry continue; if (elem instanceof LineString) { - //-- once an intersection with target exterior is recorded, skip further known-exterior points - if (hasExteriorIntersection + //-- once intersections with target exterior are recorded for both line-interior and line-boundary ends, + //-- skip further known-exterior line components (optimization) + if (hasInteriorExteriorIntersection && hasBoundaryExteriorIntersection && elem.getEnvelopeInternal().disjoint(geomTarget.getEnvelope())) continue; LineString line = (LineString) elem; Coordinate e0 = line.getCoordinateN(0); - hasExteriorIntersection |= computeLineEnd(geom, isA, e0, geomTarget, topoComputer); + int loc0 = computeLineEnd(geom, isA, e0, geomTarget, topoComputer); + if (loc0 == Location.INTERIOR) hasInteriorExteriorIntersection = true; + else if (loc0 == Location.BOUNDARY) hasBoundaryExteriorIntersection = true; if (topoComputer.isResultKnown()) { return true; } if (! line.isClosed()) { Coordinate e1 = line.getCoordinateN(line.getNumPoints() - 1); - hasExteriorIntersection |= computeLineEnd(geom, isA, e1, geomTarget, topoComputer); + int loc1 = computeLineEnd(geom, isA, e1, geomTarget, topoComputer); + if (loc1 == Location.INTERIOR) hasInteriorExteriorIntersection = true; + else if (loc1 == Location.BOUNDARY) hasBoundaryExteriorIntersection = true; if (topoComputer.isResultKnown()) { return true; } @@ -438,22 +444,26 @@ private boolean computeLineEnds(RelateGeometry geom, boolean isA, RelateGeometry * @param pt * @param geomTarget * @param topoComputer - * @return true if the line endpoint is in the exterior of the target + * @return the location of the line endpoint (INTERIOR or BOUNDARY) if it is in the exterior of the target, + * otherwise Location.NONE */ - private boolean computeLineEnd(RelateGeometry geom, boolean isA, Coordinate pt, + private int computeLineEnd(RelateGeometry geom, boolean isA, Coordinate pt, RelateGeometry geomTarget, TopologyComputer topoComputer) { int locDimLineEnd = geom.locateLineEndWithDim(pt); int dimLineEnd = DimensionLocation.dimension(locDimLineEnd, topoComputer.getDimension(isA)); //-- skip line ends which are in a GC area if (dimLineEnd != Dimension.L) - return false; + return Location.NONE; int locLineEnd = DimensionLocation.location(locDimLineEnd); int locDimTarget = geomTarget.locateWithDim(pt); int locTarget = DimensionLocation.location(locDimTarget); int dimTarget = DimensionLocation.dimension(locDimTarget, topoComputer.getDimension(! isA)); topoComputer.addLineEndOnGeometry(isA, locLineEnd, locTarget, dimTarget, pt); - return locTarget == Location.EXTERIOR; + if (locTarget == Location.EXTERIOR) { + return locLineEnd; + } + return Location.NONE; } private boolean computeAreaVertex(RelateGeometry geom, boolean isA, RelateGeometry geomTarget, TopologyComputer topoComputer) { diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTest.java index 56b9f134a5..0d43b838b7 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTest.java @@ -217,6 +217,22 @@ public void testLinesDisjointOverlappingEnvelopes() { checkTouches(a, b, false); } + /** + * Case from https://github.com/locationtech/jts/issues/1175 + * Tests that boundary points for disjoint line components are not skipped + * by the exterior intersection optimization in computeLineEnds(). + * The optimization must track interior and boundary exterior intersections separately. + */ + public void testLineDisjointMultiLineWithBoundaryInExterior_JTS1175() { + String a = "LINESTRING(10 10,20 20)"; + String b = "MULTILINESTRING((0 0,1 0),(1 0,2 0),(-1 0,0 0))"; + checkRelate(a, b, "FF1FF0102"); + checkRelate(b, a, "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