diff --git a/modules/core/src/main/java/org/locationtech/jts/noding/BasicSegmentString.java b/modules/core/src/main/java/org/locationtech/jts/noding/BasicSegmentString.java index dc199e47ae..7c735ffa0a 100644 --- a/modules/core/src/main/java/org/locationtech/jts/noding/BasicSegmentString.java +++ b/modules/core/src/main/java/org/locationtech/jts/noding/BasicSegmentString.java @@ -29,6 +29,15 @@ public class BasicSegmentString implements SegmentString { + public static BasicSegmentString substring(SegmentString segString, int start, int end) { + Coordinate[] pts = new Coordinate[end - start + 1]; + int ipts = 0; + for (int i = start; i < end + 1; i++) { + pts[ipts++] = segString.getCoordinate(i).copy(); + } + return new BasicSegmentString(pts, segString.getData()); + } + private Coordinate[] pts; private Object data; diff --git a/modules/core/src/main/java/org/locationtech/jts/noding/BoundaryChainNoder.java b/modules/core/src/main/java/org/locationtech/jts/noding/BoundaryChainNoder.java index 3eb503aa93..7a86b60083 100644 --- a/modules/core/src/main/java/org/locationtech/jts/noding/BoundaryChainNoder.java +++ b/modules/core/src/main/java/org/locationtech/jts/noding/BoundaryChainNoder.java @@ -15,6 +15,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Set; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.LineSegment; @@ -50,11 +51,17 @@ public BoundaryChainNoder() { @Override public void computeNodes(Collection segStrings) { - HashSet segSet = new HashSet(); + HashSet boundarySegSet = new HashSet(); BoundaryChainMap[] boundaryChains = new BoundaryChainMap[segStrings.size()]; - addSegments(segStrings, segSet, boundaryChains); - markBoundarySegments(segSet); + addSegments(segStrings, boundarySegSet, boundaryChains); + markBoundarySegments(boundarySegSet); chainList = extractChains(boundaryChains); + + //-- check for self-touching nodes and split chains at those nodes + Set nodePts = findNodePts(chainList); + if (nodePts.size() > 0) { + chainList = nodeChains(chainList, nodePts); + } } private static void addSegments(Collection segStrings, HashSet segSet, @@ -95,6 +102,56 @@ private static List extractChains(BoundaryChainMap[] boundaryChai return chainList; } + private Set findNodePts(List segStrings) { + Set interorVertices = new HashSet(); + Set nodes = new HashSet(); + for (SegmentString ss : segStrings) { + //-- endpoints are nodes + nodes.add(ss.getCoordinate(0)); + nodes.add(ss.getCoordinate(ss.size() - 1)); + + //-- check for duplicate interior points + for (int i = 1; i < ss.size() - 1; i++) { + Coordinate p = ss.getCoordinate(i); + if (interorVertices.contains(p)) { + nodes.add(p); + } + interorVertices.add(p); + } + } + return nodes; + } + + private List nodeChains(List chains, Set nodePts) { + List nodedChains = new ArrayList(); + for (SegmentString chain : chains) { + nodeChain(chain, nodePts, nodedChains); + } + return nodedChains; + } + + private void nodeChain(SegmentString chain, Set nodePts, List nodedChains) { + int start = 0; + while (start < chain.size() - 1) { + int end = findNodeIndex(chain, start, nodePts); + //-- if no interior nodes found, keep original chain + if (start == 0 && end == chain.size() - 1) { + nodedChains.add(chain); + return; + } + nodedChains.add(BasicSegmentString.substring(chain, start, end)); + start = end; + } + } + + private int findNodeIndex(SegmentString chain, int start, Set nodePts) { + for (int i = start + 1; i < chain.size(); i++) { + if (nodePts.contains(chain.getCoordinate(i))) + return i; + } + return chain.size() - 1; + } + @Override public Collection getNodedSubstrings() { return chainList; diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageUnionTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageUnionTest.java index cd08a60895..fffb517ed1 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageUnionTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageUnionTest.java @@ -40,6 +40,27 @@ public void testEmpty() { ); } + public void testHoleTouchingSide() { + checkUnion( + "GEOMETRYCOLLECTION (POLYGON ((1 9, 9 9, 9 6, 2 6, 1 9)), POLYGON ((1 1, 1 9, 2 6, 5 3, 9 6, 9 1, 1 1)))", + "POLYGON ((9 6, 9 1, 1 1, 1 9, 9 9, 9 6), (9 6, 2 6, 5 3, 9 6))" + ); + } + + public void testHolesTouchingSide() { + checkUnion( + "GEOMETRYCOLLECTION (POLYGON ((1 9, 9 9, 9 6, 5 7, 2 6, 1 9)), POLYGON ((1 1, 1 9, 2 6, 4 3, 5 7, 7 3, 9 6, 9 1, 1 1)))", + "POLYGON ((9 9, 9 6, 9 1, 1 1, 1 9, 9 9), (5 7, 7 3, 9 6, 5 7), (2 6, 4 3, 5 7, 2 6))" + ); + } + + public void testHolesTouching() { + checkUnion( + "GEOMETRYCOLLECTION (POLYGON ((1 9, 9 9, 9 6, 7 7, 5 7, 2 6, 1 9)), POLYGON ((1 1, 1 9, 2 6, 4 3, 5 7, 7 3, 7 7, 9 6, 9 1, 1 1)))", + "POLYGON ((9 9, 9 6, 9 1, 1 1, 1 9, 9 9), (5 7, 7 3, 7 7, 5 7), (2 6, 4 3, 5 7, 2 6))" + ); + } + private void checkUnion(String wktCoverage, String wktExpected) { Geometry covGeom = read(wktCoverage); Geometry[] coverage = toArray(covGeom); diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/CoverageUnionTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/CoverageUnionTest.java index c60415ace1..8cafcf0343 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/CoverageUnionTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/CoverageUnionTest.java @@ -30,9 +30,25 @@ public void testPolygonsConcentricHalfDonuts( ) { "MULTIPOLYGON (((1 9, 6 9, 9 9, 9 1, 6 1, 1 1, 1 9), (2 8, 2 2, 6 2, 8 2, 8 8, 6 8, 2 8)), ((5 3, 3 3, 3 7, 5 7, 7 7, 7 3, 5 3), (5 4, 6 4, 6 6, 5 6, 4 6, 4 4, 5 4)))"); } - public void testPolygonsNested( ) { - checkUnion("GEOMETRYCOLLECTION (POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9), (3 7, 3 3, 7 3, 7 7, 3 7)), POLYGON ((3 7, 7 7, 7 3, 3 3, 3 7)))", - "POLYGON ((1 1, 1 9, 9 9, 9 1, 1 1))"); + public void testHoleTouchingSide() { + checkUnion( + "GEOMETRYCOLLECTION (POLYGON ((1 9, 9 9, 9 6, 2 6, 1 9)), POLYGON ((1 1, 1 9, 2 6, 5 3, 9 6, 9 1, 1 1)))", + "POLYGON ((9 6, 9 1, 1 1, 1 9, 9 9, 9 6), (9 6, 2 6, 5 3, 9 6))" + ); + } + + public void testHolesTouchingSide() { + checkUnion( + "GEOMETRYCOLLECTION (POLYGON ((1 9, 9 9, 9 6, 5 7, 2 6, 1 9)), POLYGON ((1 1, 1 9, 2 6, 4 3, 5 7, 7 3, 9 6, 9 1, 1 1)))", + "POLYGON ((9 9, 9 6, 9 1, 1 1, 1 9, 9 9), (5 7, 7 3, 9 6, 5 7), (2 6, 4 3, 5 7, 2 6))" + ); + } + + public void testHolesTouching() { + checkUnion( + "GEOMETRYCOLLECTION (POLYGON ((1 9, 9 9, 9 6, 7 7, 5 7, 2 6, 1 9)), POLYGON ((1 1, 1 9, 2 6, 4 3, 5 7, 7 3, 7 7, 9 6, 9 1, 1 1)))", + "POLYGON ((9 9, 9 6, 9 1, 1 1, 1 9, 9 9), (5 7, 7 3, 7 7, 5 7), (2 6, 4 3, 5 7, 2 6))" + ); } public void testPolygonsFormingHole( ) { @@ -45,6 +61,13 @@ public void testPolygonsSquareGrid( ) { "POLYGON ((0 25, 0 50, 0 75, 0 100, 25 100, 50 100, 75 100, 100 100, 100 75, 100 50, 100 25, 100 0, 75 0, 50 0, 25 0, 0 0, 0 25))"); } + public void testPolygonsNested( ) { + checkUnion("GEOMETRYCOLLECTION (POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9), (3 7, 3 3, 7 3, 7 7, 3 7)), POLYGON ((3 7, 7 7, 7 3, 3 3, 3 7)))", + "POLYGON ((1 1, 1 9, 9 9, 9 1, 1 1))"); + } + + //------------------------------------------------------------ + /** * Sequential lines are still noded */ @@ -69,6 +92,8 @@ public void testLinesNetwork( ) { "MULTILINESTRING ((1 9, 3.1 8), (2 3, 4 3), (3.1 8, 5 7), (4 3, 5 3), (5 3, 5 7), (5 3, 7 4), (5 3, 8 1), (5 7, 7 8), (7 4, 9 5), (7 8, 9 9))"); } + //======================================================= + private void checkUnion(String wkt, String wktExpected) { Geometry coverage = read(wkt); Geometry expected = read(wktExpected);