/* * The JTS Topology Suite is a collection of Java classes that * implement the fundamental operations required to validate a given * geo-spatial data set to a known topological specification. * * Copyright (C) 2001 Vivid Solutions * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * For more information, contact: * * Vivid Solutions * Suite #1A * 2328 Government Street * Victoria BC V8T 5G5 * Canada * * (250)385-6040 * www.vividsolutions.com */ package com.revolsys.geometry.operation.overlay; import java.util.ArrayList; import java.util.Collection; import java.util.List; import com.revolsys.geometry.geomgraph.DirectedEdge; import com.revolsys.geometry.geomgraph.EdgeRing; import com.revolsys.geometry.geomgraph.Node; import com.revolsys.geometry.geomgraph.PlanarGraph; import com.revolsys.geometry.model.BoundingBox; import com.revolsys.geometry.model.GeometryFactory; import com.revolsys.geometry.model.LinearRing; import com.revolsys.geometry.model.Point; import com.revolsys.geometry.model.Polygon; import com.revolsys.geometry.model.TopologyException; import com.revolsys.geometry.util.Assert; /** * Forms {@link Polygon}s out of a graph of {@link DirectedEdge}s. * The edges to use are marked as being in the result Area. * <p> * * @version 1.7 */ public class PolygonBuilder { private final GeometryFactory geometryFactory; // private List dirEdgeList; // private NodeMap nodes; private final List<EdgeRing> shellList = new ArrayList<>(); public PolygonBuilder(final GeometryFactory geometryFactory) { this.geometryFactory = geometryFactory; } /** * Add a set of edges and nodes, which form a graph. * The graph is assumed to contain one or more polygons, * possibly with holes. */ public void add(final Collection<DirectedEdge> dirEdges, final Collection<Node> nodes) { PlanarGraph.linkResultDirectedEdges(nodes); final List<MaximalEdgeRing> maxEdgeRings = buildMaximalEdgeRings(dirEdges); final List<EdgeRing> freeHoleList = new ArrayList<>(); final List<MaximalEdgeRing> edgeRings = buildMinimalEdgeRings(maxEdgeRings, this.shellList, freeHoleList); sortShellsAndHoles(edgeRings, this.shellList, freeHoleList); placeFreeHoles(this.shellList, freeHoleList); } /** * Add a complete graph. * The graph is assumed to contain one or more polygons, * possibly with holes. */ public void add(final PlanarGraph graph) { add(graph.getEdgeEnds(), graph.getNodes()); } /** * for all DirectedEdges in result, form them into MaximalEdgeRings */ private List<MaximalEdgeRing> buildMaximalEdgeRings(final Collection<DirectedEdge> dirEdges) { final List<MaximalEdgeRing> maxEdgeRings = new ArrayList<>(); for (final DirectedEdge de : dirEdges) { if (de.isInResult() && de.getLabel().isArea()) { // if this edge has not yet been processed if (de.getEdgeRing() == null) { final MaximalEdgeRing er = new MaximalEdgeRing(de, this.geometryFactory); maxEdgeRings.add(er); er.setInResult(); } } } return maxEdgeRings; } private List<MaximalEdgeRing> buildMinimalEdgeRings(final List<MaximalEdgeRing> maxEdgeRings, final List<EdgeRing> shellList, final List<EdgeRing> freeHoleList) { final List<MaximalEdgeRing> edgeRings = new ArrayList<>(); for (final MaximalEdgeRing er : maxEdgeRings) { if (er.getMaxNodeDegree() > 2) { er.linkDirectedEdgesForMinimalEdgeRings(); final List<MinimalEdgeRing> minEdgeRings = er.buildMinimalRings(); // at this point we can go ahead and attempt to place holes, if this // EdgeRing is a polygon final EdgeRing shell = findShell(minEdgeRings); if (shell != null) { placePolygonHoles(shell, minEdgeRings); shellList.add(shell); } else { freeHoleList.addAll(minEdgeRings); } } else { edgeRings.add(er); } } return edgeRings; } /** * Checks the current set of shells (with their associated holes) to * see if any of them contain the point. */ public boolean containsPoint(final Point point) { for (final EdgeRing shell : this.shellList) { if (shell.containsPoint(point)) { return true; } } return false; } /** * Find the innermost enclosing shell EdgeRing containing the argument EdgeRing, if any. * The innermost enclosing ring is the <i>smallest</i> enclosing ring. * The algorithm used depends on the fact that: * <br> * ring A contains ring B iff envelope(ring A) contains envelope(ring B) * <br> * This routine is only safe to use if the chosen point of the hole * is known to be properly contained in a shell * (which is guaranteed to be the case if the hole does not touch its shell) * * @return containing EdgeRing, if there is one * or null if no containing EdgeRing is found */ private EdgeRing findEdgeRingContaining(final EdgeRing testEr, final List<EdgeRing> shellList) { final LinearRing testRing = testEr.getLinearRing(); final BoundingBox testEnv = testRing.getBoundingBox(); final Point testPt = testRing.getPoint(0); EdgeRing minShell = null; BoundingBox minEnv = null; for (final EdgeRing tryShell : shellList) { final LinearRing tryRing = tryShell.getLinearRing(); final BoundingBox tryEnv = tryRing.getBoundingBox(); if (minShell != null) { minEnv = minShell.getLinearRing().getBoundingBox(); } boolean isContained = false; if (tryEnv.covers(testEnv) && tryRing.isPointInRing(testPt)) { isContained = true; } // check if this new containing ring is smaller than the current minimum // ring if (isContained) { if (minShell == null || minEnv.covers(tryEnv)) { minShell = tryShell; } } } return minShell; } /** * This method takes a list of MinimalEdgeRings derived from a MaximalEdgeRing, * and tests whether they form a Polygon. This is the case if there is a single shell * in the list. In this case the shell is returned. * The other possibility is that they are a series of connected holes, in which case * no shell is returned. * * @return the shell EdgeRing, if there is one * or null, if all the rings are holes */ private EdgeRing findShell(final List<MinimalEdgeRing> minEdgeRings) { int shellCount = 0; EdgeRing shell = null; for (final MinimalEdgeRing er : minEdgeRings) { if (!er.isHole()) { shell = er; shellCount++; } } Assert.isTrue(shellCount <= 1, "found two shells in MinimalEdgeRing list"); return shell; } public List<Polygon> getPolygons() { final List<Polygon> polygons = new ArrayList<>(); for (final EdgeRing edgeRing : this.shellList) { final Polygon polygon = edgeRing.toPolygon(this.geometryFactory); polygons.add(polygon); } return polygons; } /** * This method determines finds a containing shell for all holes * which 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(final List<EdgeRing> shellList, final List<EdgeRing> freeHoleList) { for (final EdgeRing hole : freeHoleList) { // only place this hole if it doesn't yet have a shell if (hole.getShell() == null) { final EdgeRing shell = findEdgeRingContaining(hole, shellList); if (shell == null) { throw new TopologyException("unable to assign hole to a shell", hole.getCoordinate(0)); } // Assert.isTrue(shell != null, "unable to assign hole to a shell"); hole.setShell(shell); } } } /** * This method assigns the holes for a Polygon (formed from a list of * MinimalEdgeRings) to its shell. * Determining the holes for a MinimalEdgeRing polygon 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 void placePolygonHoles(final EdgeRing shell, final List<MinimalEdgeRing> minEdgeRings) { for (final MinimalEdgeRing er : minEdgeRings) { if (er.isHole()) { er.setShell(shell); } } } /** * For all rings in the input list, * determine whether the ring is a shell or a hole * and add it to the appropriate list. * Due to the way the DirectedEdges were linked, * a ring is a shell if it is oriented CW, a hole otherwise. */ private void sortShellsAndHoles(final List<MaximalEdgeRing> edgeRings, final List<EdgeRing> shellList, final List<EdgeRing> freeHoleList) { for (final MaximalEdgeRing edgeRing : edgeRings) { if (edgeRing.isHole()) { freeHoleList.add(edgeRing); } else { shellList.add(edgeRing); } } } }