From 55938b6406a8773159f4d6eb883c0831d53f722d Mon Sep 17 00:00:00 2001 From: grootstebozewolf Date: Thu, 4 Jun 2026 12:56:04 +0800 Subject: [PATCH] Fix RelateNG.computeLineEnds incorrectly skipping boundary points for disjoint line components (#1175) The optimization in computeLineEnds() that skips processing of line components whose envelopes are disjoint from the target after finding one exterior intersection was incorrectly skipping boundary point classification. Track hasInteriorExteriorIntersection and hasBoundaryExteriorIntersection separately so that we only skip when both kinds of exterior line-end intersections have been recorded. This ensures boundary endpoints on later disjoint components (e.g. odd-valence ends in a MultiLineString) are still processed and reported in the DE-9IM matrix (position 7, EB). - Refactor computeLineEnds() + computeLineEnd() to use two booleans and return the line-end location (INTERIOR/BOUNDARY/NONE) instead of a simple boolean. - Add the exact reproduction case from the issue as testLineDisjointMultiLineWithBoundaryInExterior_JTS1175() exercising relate() + the various predicate helpers. - Update history. Reported by peterstace; reproduction: g1 = LINESTRING(10 10,20 20) g2 = MULTILINESTRING((0 0,1 0),(1 0,2 0),(-1 0,0 0)) Expected: FF1FF0102 (EB='0'), was FF1FF01F2 (EB='F') (cherry picked from commit 29910499df06c344af62183dc8cfbf64242c3d80, isolated to just the RelateNG boundary skip changes) --- doc/JTS_Version_History.md | 1 + .../jts/operation/relateng/RelateNG.java | 28 +++++++++++++------ .../jts/operation/relateng/RelateNGTest.java | 16 +++++++++++ 3 files changed, 36 insertions(+), 9 deletions(-) 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