From 8e9b050dfb684e1217d4cee35643f5193551454f Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 9 Jun 2022 11:56:33 -0700 Subject: [PATCH] Make CoverageUnion faster with BoundaryChainNoder Signed-off-by: Martin Davis --- .../jts/noding/BoundaryChainNoder.java | 173 ++++++++++++++++++ .../jts/noding/BoundarySegmentNoder.java | 111 +++++++++++ .../operation/overlayng/CoverageUnion.java | 20 +- .../overlayng/CoverageUnionTest.java | 5 + 4 files changed, 305 insertions(+), 4 deletions(-) create mode 100644 modules/core/src/main/java/org/locationtech/jts/noding/BoundaryChainNoder.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/noding/BoundarySegmentNoder.java 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 new file mode 100644 index 0000000000..10e034dbb0 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/noding/BoundaryChainNoder.java @@ -0,0 +1,173 @@ +/* + * 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.noding; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineSegment; + +/** + * A noder which extracts chains of boundary segments + * as {@link SegmentString}s. + * Boundary segments are those which are not duplicated in the input. + * The segment strings are extracted in a way that maximises their length, + * and minimizes the total number of edges. + * This produces the most efficient topological graph structure. + *

+ * Segments which are not on the boundary are those which + * have an identical segment in another polygon ring. + *

+ * This enables fast overlay of polygonal coverages in {@link CoverageUnion}. + * This noder is faster than {@link SegmentExtractingNoder} + * and {@link BoundarySegmentNoder}. + *

+ * No precision reduction is carried out. + * If that is required, another noder must be used (such as a snap-rounding noder), + * or the input must be precision-reduced beforehand. + * + * @author Martin Davis + * + */ +public class BoundaryChainNoder implements Noder { + + private List chainList; + + /** + * Creates a new boundary-extracting noder. + */ + public BoundaryChainNoder() { + + } + + @Override + public void computeNodes(Collection segStrings) { + HashSet segSet = new HashSet(); + BoundarySegmentMap[] bdySections = new BoundarySegmentMap[segStrings.size()]; + addSegments(segStrings, segSet, bdySections); + markBoundarySegments(segSet); + chainList = extractChains(bdySections); + } + + private static void addSegments(Collection segStrings, HashSet segSet, + BoundarySegmentMap[] includedSegs) { + int i = 0; + for (SegmentString ss : segStrings) { + BoundarySegmentMap segInclude = new BoundarySegmentMap(ss); + includedSegs[i++] = segInclude; + addSegments( ss, segInclude, segSet ); + } + } + + private static void addSegments(SegmentString segString, BoundarySegmentMap segInclude, HashSet segSet) { + for (int i = 0; i < segString.size() - 1; i++) { + Coordinate p0 = segString.getCoordinate(i); + Coordinate p1 = segString.getCoordinate(i + 1); + Segment seg = new Segment(p0, p1, segInclude, i); + if (segSet.contains(seg)) { + segSet.remove(seg); + } + else { + segSet.add(seg); + } + } + } + + private static void markBoundarySegments(HashSet segSet) { + for (Segment seg : segSet) { + seg.markInBoundary(); + } + } + + private static List extractChains(BoundarySegmentMap[] sections) { + List sectionList = new ArrayList(); + for (BoundarySegmentMap sect : sections) { + sect.createChains(sectionList); + } + return sectionList; + } + + @Override + public Collection getNodedSubstrings() { + return chainList; + } + + private static class BoundarySegmentMap { + private SegmentString segString; + private boolean[] isBoundary; + + public BoundarySegmentMap(SegmentString ss) { + this.segString = ss; + isBoundary = new boolean[ss.size() - 1]; + } + + public void setBoundarySegment(int index) { + isBoundary[index] = true; + } + + public void createChains(List chainList) { + int endIndex = 0; + while (true) { + int startIndex = findChainStart(endIndex); + if (startIndex >= segString.size() - 1) + break; + endIndex = findChainEnd(startIndex); + SegmentString ss = createChain(segString, startIndex, endIndex); + chainList.add(ss); + } + } + + private static SegmentString createChain(SegmentString segString, int startIndex, int endIndex) { + Coordinate[] pts = new Coordinate[endIndex - startIndex + 1]; + int ipts = 0; + for (int i = startIndex; i < endIndex + 1; i++) { + pts[ipts++] = segString.getCoordinate(i).copy(); + } + return new BasicSegmentString(pts, segString.getData()); + } + + private int findChainStart(int index) { + while (index < isBoundary.length && ! isBoundary[index]) { + index++; + } + return index; + } + + private int findChainEnd(int index) { + index++; + while (index < isBoundary.length && isBoundary[index]) { + index++; + } + return index; + } + } + + private static class Segment extends LineSegment { + private BoundarySegmentMap segMap; + private int index; + + public Segment(Coordinate p0, Coordinate p1, + BoundarySegmentMap segMap, int index) { + super(p0, p1); + this.segMap = segMap; + this.index = index; + normalize(); + } + + public void markInBoundary() { + segMap.setBoundarySegment(index); + } + } +} diff --git a/modules/core/src/main/java/org/locationtech/jts/noding/BoundarySegmentNoder.java b/modules/core/src/main/java/org/locationtech/jts/noding/BoundarySegmentNoder.java new file mode 100644 index 0000000000..11929c0f84 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/noding/BoundarySegmentNoder.java @@ -0,0 +1,111 @@ +/* + * 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.noding; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineSegment; + +/** + * A noder which extracts boundary line segments + * as {@link SegmentString}s. + * Boundary segments are those which are not duplicated in the input. + * It is appropriate for use with valid polygonal coverages. + *

+ * No precision reduction is carried out. + * If that is required, another noder must be used (such as a snap-rounding noder), + * or the input must be precision-reduced beforehand. + * + * @author Martin Davis + * + */ +public class BoundarySegmentNoder implements Noder { + + private List segList; + + /** + * Creates a new segment-dissolving noder. + */ + public BoundarySegmentNoder() { + + } + + @Override + public void computeNodes(Collection segStrings) { + HashSet segSet = new HashSet() ; + addSegments(segStrings, segSet); + segList = extractSegments(segSet); + } + + private static void addSegments(Collection segStrings, HashSet segSet) { + for (SegmentString ss : segStrings) { + addSegments( ss, segSet ); + } + } + + private static void addSegments(SegmentString segString, HashSet segSet) { + for (int i = 0; i < segString.size() - 1; i++) { + Coordinate p0 = segString.getCoordinate(i); + Coordinate p1 = segString.getCoordinate(i + 1); + Segment seg = new Segment(p0, p1, segString, i); + if (segSet.contains(seg)) { + segSet.remove(seg); + } + else { + segSet.add(seg); + } + } + } + + private static List extractSegments(HashSet segSet) { + List segList = new ArrayList(); + for (Segment seg : segSet) { + SegmentString ss = seg.getSegmentString(); + int i = seg.getIndex(); + Coordinate p0 = ss.getCoordinate(i); + Coordinate p1 = ss.getCoordinate(i + 1); + SegmentString segStr = new BasicSegmentString(new Coordinate[] { p0, p1 }, ss.getData()); + segList.add(segStr); + } + return segList; + } + + @Override + public Collection getNodedSubstrings() { + return segList; + } + + static class Segment extends LineSegment { + private SegmentString segStr; + private int index; + + public Segment(Coordinate p0, Coordinate p1, + SegmentString segStr, int index) { + super(p0, p1); + this.segStr = segStr; + this.index = index; + normalize(); + } + + public SegmentString getSegmentString() { + return segStr; + } + + public int getIndex() { + return index; + } + } +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/CoverageUnion.java b/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/CoverageUnion.java index 1d2e9c52f9..4d8881c921 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/CoverageUnion.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/CoverageUnion.java @@ -12,6 +12,8 @@ package org.locationtech.jts.operation.overlayng; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.TopologyException; +import org.locationtech.jts.noding.BoundaryChainNoder; import org.locationtech.jts.noding.Noder; import org.locationtech.jts.noding.SegmentExtractingNoder; @@ -19,7 +21,7 @@ * Unions a valid coverage of polygons or lines * in an efficient way. *

- * A valid polygonal coverage is a collection of {@link org.locationtech.jts.geom.Polygon}s + * A polygonal coverage is a collection of {@link Polygon}s * which satisfy the following conditions: *

    *
  1. Vector-clean - Line segments within the collection @@ -28,14 +30,14 @@ * may overlap. Equivalently, polygons must be interior-disjoint. *
*

- * A valid linear coverage is a collection of {@link org.locationtech.jts.geom.LineString}s + * A linear coverage is a collection of {@link LineString}s * which satisfies the Vector-clean condition. * Note that this does not require the LineStrings to be fully noded * - i.e. they may contain coincident linework. * Coincident line segments are dissolved by the union. * Currently linear output is not merged (this may be added in a future release.) *

- * Currently no checking is done to determine whether the input is a valid coverage. + * No checking is done to determine whether the input is a valid coverage. * This is because coverage validation involves segment intersection detection, * which is much more expensive than the union phase. * If the input is not a valid coverage @@ -49,6 +51,7 @@ * * @author Martin Davis * + * @see BoundaryChainNoder * @see SegmentExtractingNoder * */ @@ -63,7 +66,16 @@ public class CoverageUnion * @throws TopologyException in some cases if the coverage is invalid */ public static Geometry union(Geometry coverage) { - Noder noder = new SegmentExtractingNoder(); + Noder noder = new BoundaryChainNoder(); + //-- these are less performant + //Noder noder = new SegmentExtractingNoder(); + //Noder noder = new BoundarySegmentNoder(); + + //-- linear networks require a segment-extracting noder + if (coverage.getDimension() < 2) { + noder = new SegmentExtractingNoder(); + } + // a precision model is not needed since no noding is done return OverlayNG.union(coverage, null, noder ); } 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 15cbe36f99..c60415ace1 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 @@ -40,6 +40,11 @@ public void testPolygonsFormingHole( ) { "POLYGON ((9 1, 1 1, 5 9, 9 1), (6 3, 5 6, 4 3, 6 3))"); } + public void testPolygonsSquareGrid( ) { + checkUnion("MULTIPOLYGON (((0 0, 0 25, 25 25, 25 0, 0 0)), ((0 25, 0 50, 25 50, 25 25, 0 25)), ((0 50, 0 75, 25 75, 25 50, 0 50)), ((0 75, 0 100, 25 100, 25 75, 0 75)), ((25 0, 25 25, 50 25, 50 0, 25 0)), ((25 25, 25 50, 50 50, 50 25, 25 25)), ((25 50, 25 75, 50 75, 50 50, 25 50)), ((25 75, 25 100, 50 100, 50 75, 25 75)), ((50 0, 50 25, 75 25, 75 0, 50 0)), ((50 25, 50 50, 75 50, 75 25, 50 25)), ((50 50, 50 75, 75 75, 75 50, 50 50)), ((50 75, 50 100, 75 100, 75 75, 50 75)), ((75 0, 75 25, 100 25, 100 0, 75 0)), ((75 25, 75 50, 100 50, 100 25, 75 25)), ((75 50, 75 75, 100 75, 100 50, 75 50)), ((75 75, 75 100, 100 100, 100 75, 75 75)))", + "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))"); + } + /** * Sequential lines are still noded */