Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make CoverageUnion faster with BoundaryChainNoder #891

Merged
merged 1 commit into from
Jun 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* Segments which are not on the boundary are those which
* have an identical segment in another polygon ring.
* <p>
* This enables fast overlay of polygonal coverages in {@link CoverageUnion}.
* This noder is faster than {@link SegmentExtractingNoder}
* and {@link BoundarySegmentNoder}.
* <p>
* 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<SegmentString> chainList;

/**
* Creates a new boundary-extracting noder.
*/
public BoundaryChainNoder() {

}

@Override
public void computeNodes(Collection segStrings) {
HashSet<Segment> segSet = new HashSet<Segment>();
BoundarySegmentMap[] bdySections = new BoundarySegmentMap[segStrings.size()];
addSegments(segStrings, segSet, bdySections);
markBoundarySegments(segSet);
chainList = extractChains(bdySections);
}

private static void addSegments(Collection<SegmentString> segStrings, HashSet<Segment> 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<Segment> 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<Segment> segSet) {
for (Segment seg : segSet) {
seg.markInBoundary();
}
}

private static List<SegmentString> extractChains(BoundarySegmentMap[] sections) {
List<SegmentString> sectionList = new ArrayList<SegmentString>();
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<SegmentString> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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<SegmentString> segList;

/**
* Creates a new segment-dissolving noder.
*/
public BoundarySegmentNoder() {

}

@Override
public void computeNodes(Collection segStrings) {
HashSet<Segment> segSet = new HashSet<Segment>() ;
addSegments(segStrings, segSet);
segList = extractSegments(segSet);
}

private static void addSegments(Collection<SegmentString> segStrings, HashSet<Segment> segSet) {
for (SegmentString ss : segStrings) {
addSegments( ss, segSet );
}
}

private static void addSegments(SegmentString segString, HashSet<Segment> 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<SegmentString> extractSegments(HashSet<Segment> segSet) {
List<SegmentString> segList = new ArrayList<SegmentString>();
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@
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;

/**
* Unions a valid coverage of polygons or lines
* in an efficient way.
* <p>
* A valid polygonal coverage is a collection of {@link org.locationtech.jts.geom.Polygon}s
* A <b>polygonal coverage</b> is a collection of {@link Polygon}s
* which satisfy the following conditions:
* <ol>
* <li><b>Vector-clean</b> - Line segments within the collection
Expand All @@ -28,14 +30,14 @@
* may overlap. Equivalently, polygons must be interior-disjoint.
* </ol>
* <p>
* A valid linear coverage is a collection of {@link org.locationtech.jts.geom.LineString}s
* A <b>linear coverage</b> is a collection of {@link LineString}s
* which satisfies the <b>Vector-clean</b> 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.)
* <p>
* 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
Expand All @@ -49,6 +51,7 @@
*
* @author Martin Davis
*
* @see BoundaryChainNoder
* @see SegmentExtractingNoder
*
*/
Expand All @@ -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 );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down