From 318a0c30e6e9d8b9f349bc04ce73a9e64a741ff0 Mon Sep 17 00:00:00 2001 From: boulea7 Date: Sun, 11 Oct 2026 20:51:00 +0800 Subject: [PATCH] Handle mixed coordinate types in OverlayNG results Signed-off-by: boulea7 --- .../jts/operation/overlayng/LineBuilder.java | 4 +- .../operation/overlayng/OverlayEdgeRing.java | 2 +- .../jts/operation/overlayng/OverlayUtil.java | 23 +- .../operation/overlayng/OverlayNGZTest.java | 258 ++++++++++++++++++ 4 files changed, 283 insertions(+), 4 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/LineBuilder.java b/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/LineBuilder.java index 16c011b332..2656f6074d 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/LineBuilder.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/LineBuilder.java @@ -240,7 +240,7 @@ private LineString toLine(OverlayEdge edge) { edge.addCoordinates(pts); Coordinate[] ptsOut = pts.toCoordinateArray(isForward); - LineString line = geometryFactory.createLineString(ptsOut); + LineString line = geometryFactory.createLineString(OverlayUtil.copyCoordinates(ptsOut)); return line; } @@ -378,4 +378,4 @@ private static int degreeOfLines(OverlayEdge node) { return degree; } -} \ No newline at end of file +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/OverlayEdgeRing.java b/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/OverlayEdgeRing.java index e4624baf80..702b066588 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/OverlayEdgeRing.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/OverlayEdgeRing.java @@ -118,7 +118,7 @@ private Coordinate[] computeRingPts(OverlayEdge start) { private void computeRing(Coordinate[] ringPts, GeometryFactory geometryFactory) { if (ring != null) return; // don't compute more than once - ring = geometryFactory.createLinearRing(ringPts); + ring = geometryFactory.createLinearRing(OverlayUtil.copyCoordinates(ringPts)); isHole = Orientation.isCCW(ring.getCoordinates()); } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/OverlayUtil.java b/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/OverlayUtil.java index 4478a06ab1..9b8751daf0 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/OverlayUtil.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/OverlayUtil.java @@ -15,6 +15,8 @@ import java.util.List; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateArrays; +import org.locationtech.jts.geom.Coordinates; import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; @@ -322,6 +324,25 @@ static Geometry createResultGeometry(List resultPolyList, List lines = new ArrayList(); for (OverlayEdge edge : graph.getEdges()) { @@ -329,7 +350,7 @@ static Geometry toLines(OverlayGraph graph, boolean isOutputEdges, GeometryFacto if (! includeEdge) continue; //Coordinate[] pts = getCoords(nss); Coordinate[] pts = edge.getCoordinatesOriented(); - LineString line = geomFact.createLineString(pts); + LineString line = geomFact.createLineString(copyCoordinates(pts)); line.setUserData(labelForResult(edge) ); lines.add(line); } diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGZTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGZTest.java index c7a7f887a1..6874a278e9 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGZTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGZTest.java @@ -11,7 +11,18 @@ */ package org.locationtech.jts.operation.overlayng; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateArrays; +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.CoordinateSequenceFilter; +import org.locationtech.jts.geom.CoordinateXY; +import org.locationtech.jts.geom.CoordinateXYM; +import org.locationtech.jts.geom.Coordinates; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.geom.PrecisionModel; +import org.locationtech.jts.geom.impl.PackedCoordinateSequenceFactory; import junit.textui.TestRunner; import test.jts.GeometryTestCase; @@ -110,6 +121,173 @@ public void testPolygonXYPolygonXYIntersection() { "POLYGON ((2 6, 3 9, 9 9, 9 5, 7 6, 2 6))"); } + public void testPolygonCoordinateXYIntersection() { + String expected = "POLYGON Z ((5 5 1, 10 5 1.5, 10 10 3, 5 10 2.5, 5 5 1))"; + checkOverlayCoordinateXY(OverlayNG.INTERSECTION, 25, expected, expected); + } + + public void testPolygonCoordinateXYUnion() { + String expected = "POLYGON Z ((0 0 2.5, 10 0 2.5, 10 5 1.5, 15 5 2, 15 15 3, 5 15 4, 5 10 2.5, 0 10 2.5, 0 0 2.5))"; + checkOverlayCoordinateXY(OverlayNG.UNION, 175, expected, expected); + } + + public void testPolygonCoordinateXYDifference() { + checkOverlayCoordinateXY(OverlayNG.DIFFERENCE, 75, + "POLYGON Z ((0 0 2.5, 10 0 2.5, 10 5 1.5, 5 5 1, 5 10 2.5, 0 10 2.5, 0 0 2.5))", + "POLYGON Z ((5 10 2.5, 10 10 3, 10 5 1.5, 15 5 2, 15 15 3, 5 15 4, 5 10 2.5))"); + } + + public void testPolygonCoordinateXYSymDifference() { + String expected = "MULTIPOLYGON Z (((0 0 2.5, 10 0 2.5, 10 5 1.5, 5 5 1, 5 10 2.5, 0 10 2.5, 0 0 2.5)), ((5 10 2.5, 10 10 3, 10 5 1.5, 15 5 2, 15 15 3, 5 15 4, 5 10 2.5)))"; + checkOverlayCoordinateXY(OverlayNG.SYMDIFFERENCE, 150, expected, expected); + } + + public void testPolygonCoordinateXYConstantZ() { + Geometry a = createPolygonXY(getGeometryFactory()); + Geometry b = read("POLYGON Z ((5 5 7, 15 5 7, 15 15 7, 5 15 7, 5 5 7))"); + for (int op = OverlayNG.INTERSECTION; op <= OverlayNG.SYMDIFFERENCE; op++) { + checkZ(OverlayNG.overlay(a, b, op), 7); + checkZ(OverlayNG.overlay(b, a, op), 7); + } + } + + public void testPolygonCoordinateXYDisjoint() { + Geometry a = createPolygonXY(getGeometryFactory()); + Geometry b = read("POLYGON Z ((20 20 7, 30 20 7, 30 30 7, 20 30 7, 20 20 7))"); + Geometry expected = read("MULTIPOLYGON (((0 0, 10 0, 10 10, 0 10, 0 0)), ((20 20, 30 20, 30 30, 20 30, 20 20)))"); + for (int order = 0; order < 2; order++) { + Geometry first = order == 0 ? a : b; + Geometry second = order == 0 ? b : a; + checkEqual(read("POLYGON EMPTY"), OverlayNG.overlay(first, second, OverlayNG.INTERSECTION)); + Geometry result = OverlayNG.overlay(first, second, OverlayNG.UNION); + checkEqual(expected, result); + assertEquals(200.0, result.getArea(), 0.0); + for (Coordinate coordinate : result.getCoordinates()) { + if (coordinate.x <= 10) { + assertTrue(Double.isNaN(coordinate.getZ())); + } else { + assertEquals(7.0, coordinate.getZ(), 0.0); + } + } + } + } + + public void testPolygonCoordinateXYWithXYZMInputsUnchanged() { + Polygon a = createPolygonXY(getGeometryFactory()); + Polygon b = (Polygon) read("POLYGON ZM ((5 5 7 11, 15 5 7 12, 15 15 NaN 13, 5 15 7 14, 5 5 7 11))"); + checkMeasuredOverlayInputs(a, b); + } + + public void testPolygonCoordinateXYMWithXYZMInputsUnchanged() { + Polygon a = createPolygonXYM(getGeometryFactory()); + Polygon b = (Polygon) read("POLYGON ZM ((5 5 7 11, 15 5 7 12, 15 15 NaN 13, 5 15 7 14, 5 5 7 11))"); + checkMeasuredOverlayInputs(a, b); + } + + public void testPolygonCoordinateXYMWithContainedXYZMInputsUnchanged() { + Polygon a = getGeometryFactory().createPolygon(new Coordinate[] { + new CoordinateXYM(0, 0, 101), new CoordinateXYM(20, 0, 102), + new CoordinateXYM(20, 20, 103), new CoordinateXYM(0, 20, 104), + new CoordinateXYM(0, 0, 101) + }); + Polygon b = (Polygon) read("POLYGON ZM ((5 5 7 11, 15 5 7 12, 15 15 NaN 13, 5 15 7 14, 5 5 7 11))"); + Coordinate[] originalsA = a.getCoordinates(); + Coordinate[] originalsB = b.getCoordinates(); + Coordinate[] snapshotA = CoordinateArrays.copyDeep(originalsA); + Coordinate[] snapshotB = CoordinateArrays.copyDeep(originalsB); + Geometry expected = read("POLYGON ZM ((5 5 7 11, 15 5 7 12, 15 15 7 13, 5 15 7 14, 5 5 7 11))"); + for (int order = 0; order < 2; order++) { + Geometry result = order == 0 + ? OverlayNG.overlay(a, b, OverlayNG.INTERSECTION) + : OverlayNG.overlay(b, a, OverlayNG.INTERSECTION); + checkEqualXYZM(expected, result); + checkCoordinatesUnchanged(a.getExteriorRing().getCoordinateSequence(), originalsA, snapshotA, 3, 1); + checkCoordinatesUnchanged(b.getExteriorRing().getCoordinateSequence(), originalsB, snapshotB, 4, 1); + } + } + + public void testPolygonCoordinateXYMWithXYZ() { + Polygon a = createPolygonXYM(getGeometryFactory()); + Geometry b = read("POLYGON Z ((5 5 7, 15 5 7, 15 15 7, 5 15 7, 5 5 7))"); + for (int order = 0; order < 2; order++) { + Polygon result = (Polygon) (order == 0 + ? OverlayNG.overlay(a, b, OverlayNG.UNION) + : OverlayNG.overlay(b, a, OverlayNG.UNION)); + assertEquals(4, result.getExteriorRing().getCoordinateSequence().getDimension()); + assertEquals(1, result.getExteriorRing().getCoordinateSequence().getMeasures()); + checkZ(result, 7); + assertEquals(102.0, findCoordinate(result, 10, 0).getM(), 0.0); + assertTrue(Double.isNaN(findCoordinate(result, 15, 15).getM())); + } + } + + public void testLineCoordinateXYUnion() { + Geometry a = getGeometryFactory().createLineString(new Coordinate[] { + new CoordinateXY(0, 0), new CoordinateXY(10, 0) + }); + Geometry b = read("LINESTRING Z (5 0 7, 15 0 7)"); + Geometry expected = read("MULTILINESTRING Z ((0 0 7, 5 0 7), (5 0 7, 10 0 7), (10 0 7, 15 0 7))"); + checkEqualXYZ(expected, OverlayNG.overlay(a, b, OverlayNG.UNION)); + checkEqualXYZ(expected, OverlayNG.overlay(b, a, OverlayNG.UNION)); + } + + public void testPolygonMixedCoordinatesPackedFactory() { + GeometryFactory factory = new GeometryFactory(PackedCoordinateSequenceFactory.DOUBLE_FACTORY); + Polygon a = createPolygonXYM(factory); + Geometry b = read("POLYGON ZM ((5 5 7 11, 15 5 7 12, 15 15 7 13, 5 15 7 14, 5 5 7 11))"); + Polygon result = (Polygon) OverlayNG.overlay(a, b, OverlayNG.UNION); + assertSame(factory, result.getFactory()); + assertEquals(4, result.getExteriorRing().getCoordinateSequence().getDimension()); + assertEquals(1, result.getExteriorRing().getCoordinateSequence().getMeasures()); + checkZ(result, 7); + assertEquals(102.0, findCoordinate(result, 10, 0).getM(), 0.0); + assertEquals(13.0, findCoordinate(result, 15, 15).getM(), 0.0); + } + + public void testPolygonCoordinateXYFixedPrecision() { + Geometry a = createPolygonXY(getGeometryFactory()); + Geometry b = read("POLYGON Z ((5 5 7, 15 5 7, 15 15 7, 5 15 7, 5 5 7))"); + Geometry result = OverlayNG.overlay(a, b, OverlayNG.UNION, new PrecisionModel(1)); + checkEqualXYZ(read("POLYGON Z ((0 0 7, 10 0 7, 10 5 7, 15 5 7, 15 15 7, 5 15 7, 5 10 7, 0 10 7, 0 0 7))"), result); + } + + public void testMixedCoordinateOutputEdges() { + Polygon a = createPolygonXYM(getGeometryFactory()); + Polygon b = (Polygon) read("POLYGON ZM ((5 5 7 11, 15 5 7 12, 15 15 7 13, 5 15 7 14, 5 5 7 11))"); + Coordinate[] originalsA = a.getCoordinates(); + Coordinate[] originalsB = b.getCoordinates(); + Coordinate[] snapshotA = CoordinateArrays.copyDeep(originalsA); + Coordinate[] snapshotB = CoordinateArrays.copyDeep(originalsB); + for (int mode = 0; mode < 3; mode++) { + OverlayNG overlay = new OverlayNG(a, b, OverlayNG.UNION); + if (mode == 0) overlay.setOutputEdges(true); + if (mode == 1) overlay.setOutputNodedEdges(true); + if (mode == 2) overlay.setOutputResultEdges(true); + Geometry result = overlay.getResult(); + assertFalse(result.isEmpty()); + result.apply(new CoordinateSequenceFilter() { + @Override + public void filter(CoordinateSequence sequence, int i) { + Coordinate coordinate = sequence.getCoordinate(i); + assertEquals(sequence.getDimension(), Coordinates.dimension(coordinate)); + assertEquals(sequence.getMeasures(), Coordinates.measures(coordinate)); + } + + @Override + public boolean isDone() { + return false; + } + + @Override + public boolean isGeometryChanged() { + return false; + } + }); + checkCoordinatesUnchanged(a.getExteriorRing().getCoordinateSequence(), originalsA, snapshotA, 3, 1); + checkCoordinatesUnchanged(b.getExteriorRing().getCoordinateSequence(), originalsB, snapshotB, 4, 1); + } + } + //================================================= private void checkIntersection(String wktA, String wktB, String wktExpected) { @@ -130,4 +308,84 @@ private void checkOverlay(int opCode, String wktA, String wktB, String wktExpect Geometry expected = read(wktExpected); checkEqualXYZ(expected, result); } + + private Polygon createPolygonXY(GeometryFactory factory) { + return factory.createPolygon(new Coordinate[] { + new CoordinateXY(0, 0), new CoordinateXY(10, 0), new CoordinateXY(10, 10), + new CoordinateXY(0, 10), new CoordinateXY(0, 0) + }); + } + + private Polygon createPolygonXYM(GeometryFactory factory) { + return factory.createPolygon(new Coordinate[] { + new CoordinateXYM(0, 0, 101), new CoordinateXYM(10, 0, 102), + new CoordinateXYM(10, 10, 103), new CoordinateXYM(0, 10, 104), + new CoordinateXYM(0, 0, 101) + }); + } + + private void checkOverlayCoordinateXY(int opCode, double area, + String expected, String expectedReverse) { + Geometry a = createPolygonXY(getGeometryFactory()); + Geometry b = read("POLYGON Z ((5 5 1, 15 5 2, 15 15 3, 5 15 4, 5 5 1))"); + Geometry result = OverlayNG.overlay(a, b, opCode); + checkEqualXYZ(read(expected), result); + assertEquals(area, result.getArea(), 0.0); + Geometry resultReverse = OverlayNG.overlay(b, a, opCode); + checkEqualXYZ(read(expectedReverse), resultReverse); + assertEquals(area, resultReverse.getArea(), 0.0); + } + + private void checkMeasuredOverlayInputs(Polygon a, Polygon b) { + CoordinateSequence seqA = a.getExteriorRing().getCoordinateSequence(); + CoordinateSequence seqB = b.getExteriorRing().getCoordinateSequence(); + Coordinate[] originalsA = a.getCoordinates(); + Coordinate[] originalsB = b.getCoordinates(); + Coordinate[] snapshotA = CoordinateArrays.copyDeep(originalsA); + Coordinate[] snapshotB = CoordinateArrays.copyDeep(originalsB); + int dimensionA = seqA.getDimension(); + int measuresA = seqA.getMeasures(); + for (int order = 0; order < 2; order++) { + Polygon result = (Polygon) (order == 0 + ? OverlayNG.overlay(a, b, OverlayNG.UNION) + : OverlayNG.overlay(b, a, OverlayNG.UNION)); + assertEquals(4, result.getExteriorRing().getCoordinateSequence().getDimension()); + assertEquals(1, result.getExteriorRing().getCoordinateSequence().getMeasures()); + checkZ(result, 7); + assertEquals(13.0, findCoordinate(result, 15, 15).getM(), 0.0); + assertEquals(snapshotA[1].getM(), findCoordinate(result, 10, 0).getM(), 0.0); + checkCoordinatesUnchanged(seqA, originalsA, snapshotA, dimensionA, measuresA); + checkCoordinatesUnchanged(seqB, originalsB, snapshotB, 4, 1); + } + } + + private void checkCoordinatesUnchanged(CoordinateSequence sequence, + Coordinate[] originals, Coordinate[] snapshot, int dimension, int measures) { + assertEquals(dimension, sequence.getDimension()); + assertEquals(measures, sequence.getMeasures()); + for (int i = 0; i < snapshot.length; i++) { + Coordinate actual = sequence.getCoordinate(i); + assertSame(originals[i], actual); + assertEquals(snapshot[i].getClass(), actual.getClass()); + assertEquals(snapshot[i].x, actual.x, 0.0); + assertEquals(snapshot[i].y, actual.y, 0.0); + assertEquals(snapshot[i].getZ(), actual.getZ(), 0.0); + assertEquals(snapshot[i].getM(), actual.getM(), 0.0); + } + } + + private void checkZ(Geometry geometry, double z) { + assertFalse(geometry.isEmpty()); + for (Coordinate coordinate : geometry.getCoordinates()) { + assertEquals(z, coordinate.getZ(), 0.0); + } + } + + private Coordinate findCoordinate(Geometry geometry, double x, double y) { + for (Coordinate coordinate : geometry.getCoordinates()) { + if (coordinate.x == x && coordinate.y == y) return coordinate; + } + fail("Missing coordinate (" + x + ", " + y + ")"); + return null; + } }