/* * 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.geomgraph; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import com.revolsys.geometry.algorithm.BoundaryNodeRule; import com.revolsys.geometry.algorithm.LineIntersector; import com.revolsys.geometry.algorithm.PointLocator; import com.revolsys.geometry.algorithm.locate.IndexedPointInAreaLocator; import com.revolsys.geometry.algorithm.locate.PointOnGeometryLocator; import com.revolsys.geometry.geomgraph.index.EdgeSetIntersector; import com.revolsys.geometry.geomgraph.index.SegmentIntersector; import com.revolsys.geometry.geomgraph.index.SimpleMCSweepLineIntersector; import com.revolsys.geometry.model.Geometry; import com.revolsys.geometry.model.LineString; import com.revolsys.geometry.model.LinearRing; import com.revolsys.geometry.model.Location; import com.revolsys.geometry.model.Point; import com.revolsys.geometry.model.Polygon; import com.revolsys.geometry.model.Polygonal; /** * A GeometryGraph is a graph that models a given Geometry * @version 1.7 */ public class GeometryGraph extends PlanarGraph { /** * This method implements the Boundary Determination Rule * for determining whether * a component (node or edge) that appears multiple times in elements * of a MultiGeometry is in the boundary or the interior of the Geometry * <br> * The SFS uses the "Mod-2 Rule", which this function implements * <br> * An alternative (and possibly more intuitive) rule would be * the "At Most One Rule": * isInBoundary = (componentCount == 1) */ /* * public static boolean isInBoundary(int boundaryCount) { // the "Mod-2 Rule" return * boundaryCount % 2 == 1; } public static int determineBoundary(int boundaryCount) { return * isInBoundary(boundaryCount) ? Location.BOUNDARY : Location.INTERIOR; } */ public static Location determineBoundary(final BoundaryNodeRule boundaryNodeRule, final int boundaryCount) { return boundaryNodeRule.isInBoundary(boundaryCount) ? Location.BOUNDARY : Location.INTERIOR; } private PointOnGeometryLocator areaPtLocator = null; private final int argIndex; // the index of this geometry as an argument to a private BoundaryNodeRule boundaryNodeRule = null; private Collection<Node> boundaryNodes; private final Geometry geometry; // spatial function (used for labelling) private boolean hasTooFewPoints = false; private Point invalidPoint = null; /** * The lineEdgeMap is a map of the linestring components of the * parentGeometry to the edges which are derived from them. * This is used to efficiently perform findEdge queries */ private final Map<LineString, Edge> lineEdgeMap = new HashMap<>(); // for use if geometry is not Polygonal private final PointLocator ptLocator = new PointLocator(); /** * If this flag is true, the Boundary Determination Rule will used when deciding * whether nodes are in the boundary or not */ private boolean useBoundaryDeterminationRule = true; public GeometryGraph(final int argIndex, final Geometry geometry) { this(argIndex, geometry, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE); } public GeometryGraph(final int argIndex, final Geometry geometry, final BoundaryNodeRule boundaryNodeRule) { this.argIndex = argIndex; this.geometry = geometry; this.boundaryNodeRule = boundaryNodeRule; if (geometry != null) { add(geometry); } } private void add(final Geometry geometry) { if (!geometry.isEmpty()) { if (geometry instanceof Polygon) { addPolygon((Polygon)geometry); } else if (geometry instanceof LineString) { addLineString((LineString)geometry); } else if (geometry instanceof Point) { addPoint((Point)geometry); } else if (geometry instanceof Polygonal) { this.useBoundaryDeterminationRule = false; addCollection(geometry); } else if (geometry.isGeometryCollection()) { addCollection(geometry); } else { throw new UnsupportedOperationException(geometry.getClass().getName()); } } } private void addCollection(final Geometry geometry) { for (int i = 0; i < geometry.getGeometryCount(); i++) { final Geometry part = geometry.getGeometry(i); add(part); } } /** * Add an Edge computed externally. The label on the Edge is assumed * to be correct. */ public void addEdge(final Edge edge) { insertEdge(edge); // insert the endpoint as a node, to mark that it is on the boundary insertPoint(this.argIndex, edge.getPoint(0), Location.BOUNDARY); insertPoint(this.argIndex, edge.getPoint(edge.getVertexCount() - 1), Location.BOUNDARY); } private void addLineString(final LineString line) { final LineString cleanLine = line.removeDuplicatePoints(); if (cleanLine.getVertexCount() < 2 || cleanLine.isEmpty()) { this.hasTooFewPoints = true; this.invalidPoint = cleanLine.getPoint(0); return; } else { // add the edge for the LineString // line edges do not have locations for their left and right sides final Edge e = new Edge(cleanLine, new Label(this.argIndex, Location.INTERIOR)); this.lineEdgeMap.put(line, e); insertEdge(e); /** * Add the boundary points of the LineString, if any. * Even if the LineString is closed, add both points as if they were endpoints. * This allows for the case that the node already exists and is a boundary point. */ insertBoundaryPoint(this.argIndex, cleanLine.getPoint(0)); insertBoundaryPoint(this.argIndex, cleanLine.getPoint(cleanLine.getVertexCount() - 1)); } } /** * Add a point computed externally. The point is assumed to be a * Point Geometry part, which has a location of INTERIOR. */ public void addPoint(final Point pt) { insertPoint(this.argIndex, pt, Location.INTERIOR); } private void addPolygon(final Polygon p) { addPolygonRing(p.getShell(), Location.EXTERIOR, Location.INTERIOR); for (int i = 0; i < p.getHoleCount(); i++) { final LinearRing hole = p.getHole(i); // Holes are topologically labelled opposite to the shell, since // the interior of the polygon lies on their opposite side // (on the left, if the hole is oriented CW) addPolygonRing(hole, Location.INTERIOR, Location.EXTERIOR); } } /** * Adds a polygon ring to the graph. * Empty rings are ignored. * * The left and right topological location arguments assume that the ring is oriented CW. * If the ring is in the opposite orientation, * the left and right locations must be interchanged. */ private void addPolygonRing(final LinearRing ring, final Location cwLeft, final Location cwRight) { if (!ring.isEmpty()) { final LineString simplifiedRing; try { simplifiedRing = ring.removeDuplicatePoints(); } catch (final IllegalArgumentException e) { this.hasTooFewPoints = true; this.invalidPoint = ring.getPoint(0); return; } if (simplifiedRing.getVertexCount() < 4) { this.hasTooFewPoints = true; this.invalidPoint = simplifiedRing.getPoint(0); return; } else { Location left; Location right; if (ring.isCounterClockwise()) { left = cwRight; right = cwLeft; } else { left = cwLeft; right = cwRight; } final Label label = new Label(this.argIndex, Location.BOUNDARY, left, right); final Edge e = new Edge(simplifiedRing, label); this.lineEdgeMap.put(ring, e); insertEdge(e); // insert the endpoint as a node, to mark that it is on the boundary insertPoint(this.argIndex, simplifiedRing.getPoint(0), Location.BOUNDARY); } } } /** * Add a node for a self-intersection. * If the node is a potential boundary node (e.g. came from an edge which * is a boundary) then insert it as a potential boundary node. * Otherwise, just add it as a regular node. */ private void addSelfIntersectionNode(final int argIndex, final Point point, final Location loc) { // if this node is already a boundary node, don't change it if (isBoundaryNode(argIndex, point)) { return; } if (loc == Location.BOUNDARY && this.useBoundaryDeterminationRule) { insertBoundaryPoint(argIndex, point); } else { insertPoint(argIndex, point, loc); } } private void addSelfIntersectionNodes(final int argIndex) { for (final Edge e : this.edges) { final Location eLoc = e.getLabel().getLocation(argIndex); for (final EdgeIntersection ei : e.getEdgeIntersectionList()) { addSelfIntersectionNode(argIndex, ei.newPoint2D(), eLoc); } } } public SegmentIntersector computeEdgeIntersections(final GeometryGraph g, final LineIntersector li, final boolean includeProper) { final SegmentIntersector si = new SegmentIntersector(li, includeProper, true); si.setBoundaryNodes(this.getBoundaryNodes(), g.getBoundaryNodes()); final EdgeSetIntersector esi = newEdgeSetIntersector(); esi.computeIntersections(this.edges, g.edges, si); /* * for (Iterator i = g.edges.iterator(); i.hasNext();) { Edge e = (Edge) i.next(); * Debug.print(e.getEdgeIntersectionList()); } */ return si; } /** * Compute self-nodes, taking advantage of the Geometry type to * minimize the number of intersection tests. (E.g. rings are * not tested for self-intersection, since they are assumed to be valid). * @param li the LineIntersector to use * @param computeRingSelfNodes if <false>, intersection checks are optimized to not test rings for self-intersection * @return the SegmentIntersector used, containing information about the intersections found */ public SegmentIntersector computeSelfNodes(final LineIntersector li, final boolean computeRingSelfNodes) { final SegmentIntersector si = new SegmentIntersector(li, true, false); final EdgeSetIntersector esi = newEdgeSetIntersector(); // optimized test for Polygons and Rings if (!computeRingSelfNodes && (this.geometry instanceof LinearRing || this.geometry instanceof Polygonal)) { esi.computeIntersections(this.edges, si, false); } else { esi.computeIntersections(this.edges, si, true); } // System.out.println("SegmentIntersector # tests = " + si.numTests); addSelfIntersectionNodes(this.argIndex); return si; } public void computeSplitEdges(final List<Edge> edgelist) { for (final Edge edge : this.edges) { final EdgeIntersectionList edgeIntersectionList = edge.getEdgeIntersectionList(); edgeIntersectionList.addSplitEdges(edgelist); } } public Edge findEdge(final LineString line) { return this.lineEdgeMap.get(line); } public BoundaryNodeRule getBoundaryNodeRule() { return this.boundaryNodeRule; } public Collection<Node> getBoundaryNodes() { if (this.boundaryNodes == null) { final NodeMap nodes = getNodeMap(); this.boundaryNodes = nodes.getBoundaryNodes(this.argIndex); } return this.boundaryNodes; } public Geometry getGeometry() { return this.geometry; } public Point getInvalidPoint() { return this.invalidPoint; } public boolean hasTooFewPoints() { return this.hasTooFewPoints; } /** * Adds candidate boundary points using the current {@link BoundaryNodeRule}. * This is used to add the boundary * points of dim-1 geometries (Curves/MultiCurves). */ private void insertBoundaryPoint(final int argIndex, final Point coord) { final NodeMap nodes = getNodeMap(); final Node n = nodes.addNode(coord); // nodes always have labels final Label lbl = n.getLabel(); // the new point to insert is on a boundary int boundaryCount = 1; // determine the current location for the point (if any) Location loc = Location.NONE; loc = lbl.getLocation(argIndex, Position.ON); if (loc == Location.BOUNDARY) { boundaryCount++; } // determine the boundary status of the point according to the Boundary // Determination Rule final Location newLoc = determineBoundary(this.boundaryNodeRule, boundaryCount); lbl.setLocation(argIndex, newLoc); } private void insertPoint(final int argIndex, final Point coord, final Location onLocation) { final NodeMap nodes = getNodeMap(); final Node n = nodes.addNode(coord); final Label lbl = n.getLabel(); if (lbl == null) { n.label = new Label(argIndex, onLocation); } else { lbl.setLocation(argIndex, onLocation); } } // MD - experimental for now /** * Determines the {@link Location} of the given {@link Coordinates} * in this geometry. * * @param p the point to test * @return the location of the point in the geometry */ public Location locate(final Point pt) { if (this.geometry instanceof Polygonal && this.geometry.getGeometryCount() > 50) { // lazily init point locator if (this.areaPtLocator == null) { this.areaPtLocator = new IndexedPointInAreaLocator(this.geometry); } return this.areaPtLocator.locate(pt); } return this.ptLocator.locate(pt, this.geometry); } private EdgeSetIntersector newEdgeSetIntersector() { // various options for computing intersections, from slowest to fastest // private EdgeSetIntersector esi = new SimpleEdgeSetIntersector(); // private EdgeSetIntersector esi = new MonotoneChainIntersector(); // private EdgeSetIntersector esi = new NonReversingChainIntersector(); // private EdgeSetIntersector esi = new SimpleSweepLineIntersector(); // private EdgeSetIntersector esi = new MCSweepLineIntersector(); // return new SimpleEdgeSetIntersector(); return new SimpleMCSweepLineIntersector(); } }