PolygonBuilder.java
/*
* Copyright (c) 2019 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.operation.overlayng;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.TopologyException;
import org.locationtech.jts.util.Assert;
class PolygonBuilder {
private GeometryFactory geometryFactory;
private List<OverlayEdgeRing> shellList = new ArrayList<OverlayEdgeRing>();
private List<OverlayEdgeRing> freeHoleList = new ArrayList<OverlayEdgeRing>();
private boolean isEnforcePolygonal = true;
public PolygonBuilder(List<OverlayEdge> resultAreaEdges, GeometryFactory geomFact) {
this(resultAreaEdges, geomFact, true);
}
public PolygonBuilder(List<OverlayEdge> resultAreaEdges, GeometryFactory geomFact, boolean isEnforcePolygonal) {
this.geometryFactory = geomFact;
this.isEnforcePolygonal = isEnforcePolygonal;
buildRings(resultAreaEdges);
}
public List<Polygon> getPolygons() {
return computePolygons(shellList);
}
public List<OverlayEdgeRing> getShellRings() {
return shellList;
}
private List<Polygon> computePolygons(List<OverlayEdgeRing> shellList)
{
List<Polygon> resultPolyList = new ArrayList<Polygon>();
// add Polygons for all shells
for (OverlayEdgeRing er : shellList ) {
Polygon poly = er.toPolygon(geometryFactory);
resultPolyList.add(poly);
}
return resultPolyList;
}
private void buildRings(List<OverlayEdge> resultAreaEdges)
{
linkResultAreaEdgesMax(resultAreaEdges);
List<MaximalEdgeRing> maxRings = buildMaximalRings(resultAreaEdges);
buildMinimalRings(maxRings);
placeFreeHoles(shellList, freeHoleList);
//Assert: every hole on freeHoleList has a shell assigned to it
}
private void linkResultAreaEdgesMax(List<OverlayEdge> resultEdges) {
for (OverlayEdge edge : resultEdges ) {
//Assert.isTrue(edge.isInResult());
// TODO: find some way to skip nodes which are already linked
MaximalEdgeRing.linkResultAreaMaxRingAtNode(edge);
}
}
/**
* For all OverlayEdges in result, form them into MaximalEdgeRings
*/
private static List<MaximalEdgeRing> buildMaximalRings(Collection<OverlayEdge> edges)
{
List<MaximalEdgeRing> edgeRings = new ArrayList<MaximalEdgeRing>();
for (OverlayEdge e : edges) {
if (e.isInResultArea() && e.getLabel().isBoundaryEither() ) {
// if this edge has not yet been processed
if (e.getEdgeRingMax() == null) {
MaximalEdgeRing er = new MaximalEdgeRing(e);
edgeRings.add(er);
}
}
}
return edgeRings;
}
private void buildMinimalRings(List<MaximalEdgeRing> maxRings)
{
for (MaximalEdgeRing erMax : maxRings) {
List<OverlayEdgeRing> minRings = erMax.buildMinimalRings(geometryFactory);
assignShellsAndHoles(minRings);
}
}
private void assignShellsAndHoles(List<OverlayEdgeRing> minRings) {
/**
* Two situations may occur:
* - the rings are a shell and some holes
* - rings are a set of holes
* This code identifies the situation
* and places the rings appropriately
*/
OverlayEdgeRing shell = findSingleShell(minRings);
if (shell != null) {
assignHoles(shell, minRings);
shellList.add(shell);
}
else {
// all rings are holes; their shell will be found later
freeHoleList.addAll(minRings);
}
}
/**
* Finds the single shell, if any, out of
* a list of minimal rings derived from a maximal ring.
* The other possibility is that they are a set of (connected) holes,
* in which case no shell will be found.
*
* @return the shell ring, if there is one
* or null, if all rings are holes
*/
private OverlayEdgeRing findSingleShell(List<OverlayEdgeRing> edgeRings)
{
int shellCount = 0;
OverlayEdgeRing shell = null;
for ( OverlayEdgeRing er : edgeRings ) {
if (! er.isHole()) {
shell = er;
shellCount++;
}
}
Assert.isTrue(shellCount <= 1, "found two shells in EdgeRing list");
return shell;
}
/**
* For the set of minimal rings comprising a maximal ring,
* assigns the holes to the shell known to contain them.
* Assigning the holes directly to the shell serves two purposes:
* <ul>
* <li>it is faster than using a point-in-polygon check later on.
* <li>it ensures correctness, since if the PIP test was used the point
* chosen might lie on the shell, which might return an incorrect result from the
* PIP test
* </ul>
*/
private static void assignHoles(OverlayEdgeRing shell, List<OverlayEdgeRing> edgeRings)
{
for (OverlayEdgeRing er : edgeRings) {
if (er.isHole()) {
er.setShell(shell);
}
}
}
/**
* Place holes have not yet been assigned to a shell.
* These "free" holes should
* all be <b>properly</b> contained in their parent shells, so it is safe to use the
* <code>findEdgeRingContaining</code> method.
* (This is the case because any holes which are NOT
* properly contained (i.e. are connected to their
* parent shell) would have formed part of a MaximalEdgeRing
* and been handled in a previous step).
*
* @throws TopologyException if a hole cannot be assigned to a shell
*/
private void placeFreeHoles(List<OverlayEdgeRing> shellList, List<OverlayEdgeRing> freeHoleList)
{
// TODO: use a spatial index to improve performance
for (OverlayEdgeRing hole : freeHoleList ) {
// only place this hole if it doesn't yet have a shell
if (hole.getShell() == null) {
OverlayEdgeRing shell = hole.findEdgeRingContaining(shellList);
// only when building a polygon-valid result
if (isEnforcePolygonal && shell == null) {
throw new TopologyException("unable to assign free hole to a shell", hole.getCoordinate());
}
hole.setShell(shell);
}
}
}
}