/* * 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.buffer; /** * @version 1.7 */ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import com.revolsys.geometry.algorithm.CGAlgorithms; import com.revolsys.geometry.algorithm.CGAlgorithmsDD; import com.revolsys.geometry.algorithm.LineIntersector; import com.revolsys.geometry.algorithm.RobustLineIntersector; import com.revolsys.geometry.geomgraph.DirectedEdge; import com.revolsys.geometry.geomgraph.Edge; import com.revolsys.geometry.geomgraph.EdgeList; import com.revolsys.geometry.geomgraph.Label; import com.revolsys.geometry.geomgraph.Node; import com.revolsys.geometry.geomgraph.PlanarGraph; import com.revolsys.geometry.geomgraph.Position; import com.revolsys.geometry.model.BoundingBox; import com.revolsys.geometry.model.Geometry; import com.revolsys.geometry.model.GeometryFactory; import com.revolsys.geometry.model.LineString; import com.revolsys.geometry.model.Location; import com.revolsys.geometry.model.Point; import com.revolsys.geometry.model.Polygon; import com.revolsys.geometry.model.TopologyException; import com.revolsys.geometry.model.segment.LineSegment; import com.revolsys.geometry.model.segment.LineSegmentDouble; import com.revolsys.geometry.noding.IntersectionAdder; import com.revolsys.geometry.noding.MCIndexNoder; import com.revolsys.geometry.noding.NodedSegmentString; import com.revolsys.geometry.noding.Noder; import com.revolsys.geometry.noding.ScaledNoder; import com.revolsys.geometry.noding.SegmentString; import com.revolsys.geometry.noding.snapround.MCIndexSnapRounder; import com.revolsys.geometry.operation.overlay.OverlayNodeFactory; import com.revolsys.geometry.operation.overlay.PolygonBuilder; import com.revolsys.util.MathUtil; //import debug.*; /** * Computes the buffer of a geometry, for both positive and negative buffer distances. * <p> * In GIS, the positive (or negative) buffer of a geometry is defined as * the Minkowski sum (or difference) of the geometry * with a circle of radius equal to the absolute value of the buffer distance. * In the CAD/CAM world buffers are known as </i>offset curves</i>. * In morphological analysis the * operation of postive and negative buffering * is referred to as <i>erosion</i> and <i>dilation</i> * <p> * The buffer operation always returns a polygonal result. * The negative or zero-distance buffer of lines and points is always an empty {@link Polygon}. * <p> * Since true buffer curves may contain circular arcs, * computed buffer polygons can only be approximations to the true geometry. * The user can control the accuracy of the curve approximation by specifying * the number of linear segments used to approximate curves. * <p> * The <b>end cap style</b> of a linear buffer may be specified. The * following end cap styles are supported: * <ul * <li>{@link BufferParameters#CAP_ROUND} - the usual round end caps * <li>{@link BufferParameters#CAP_FLAT} - end caps are truncated flat at the line ends * <li>{@link BufferParameters#CAP_SQUARE} - end caps are squared off at the buffer distance beyond the line ends * </ul> * <p> * * @version 1.7 */ public class Buffer { /** * A number of digits of precision which leaves some computational "headroom" * for floating point operations. * * This value should be less than the decimal precision of double-precision values (16). */ private static int MAX_PRECISION_DIGITS = 12; /** * Comutes the buffer for a geometry for a given buffer distance * and accuracy of approximation. * * @param g the geometry to buffer * @param distance the buffer distance * @param parameters the buffer parameters to use * @return the buffer of the input geometry * */ @SuppressWarnings("unchecked") public static <G extends Geometry> G buffer(final Geometry geometry, final double distance, final BufferParameters parameters) { final GeometryFactory geometryFactory = geometry.getGeometryFactory(); try { final MCIndexNoder noder = new MCIndexNoder(); final LineIntersector li = new RobustLineIntersector(geometryFactory.getScaleXY()); noder.setSegmentIntersector(new IntersectionAdder(li)); return (G)buffer(noder, geometryFactory, geometry, distance, parameters); } catch (final RuntimeException e) { if (geometryFactory.isFloating()) { return (G)bufferReducedPrecision(geometry, distance, parameters); } else { return (G)bufferFixedPrecision(geometryFactory, geometry, distance, parameters); } } } private static Geometry buffer(final Noder noder, final GeometryFactory precisionModel, final Geometry geometry, final double distance, final BufferParameters parameters) { final GeometryFactory geometryFactory = geometry.getGeometryFactory(); final OffsetCurveSetBuilder curveSetBuilder = new OffsetCurveSetBuilder(geometry, distance, precisionModel, parameters); final List<NodedSegmentString> curves = curveSetBuilder.getCurves(); if (curves.size() == 0) { return geometryFactory.polygon(); } else { final EdgeList edgeList = new EdgeList(); computeNodedEdges(noder, edgeList, curves); final PlanarGraph graph = new PlanarGraph(new OverlayNodeFactory()); final List<Edge> edges = edgeList.getEdges(); graph.addEdges(edges); final List<BufferSubgraph> subgraphList = newSubgraphs(graph); final PolygonBuilder polyBuilder = new PolygonBuilder(geometryFactory); buildSubgraphs(subgraphList, polyBuilder); final List<Polygon> polygons = polyBuilder.getPolygons(); if (polygons.size() == 0) { return geometryFactory.polygon(); } else { final Geometry resultGeom = geometryFactory.buildGeometry(polygons); return resultGeom; } } } private static Geometry bufferFixedPrecision(final GeometryFactory precisionModel, final Geometry geometry, final double distance, final BufferParameters parameters) { final MCIndexSnapRounder rounder = new MCIndexSnapRounder(1.0); final double scale = precisionModel.getScaleXY(); final Noder noder = new ScaledNoder(rounder, scale); return buffer(noder, precisionModel, geometry, distance, parameters); } private static Geometry bufferReducedPrecision(final Geometry geometry, final double distance, final BufferParameters parameters) { TopologyException saveException = null; // try and compute with decreasing precision for (int precDigits = MAX_PRECISION_DIGITS; precDigits >= 0; precDigits--) { try { final double sizeBasedScaleFactor = precisionScaleFactor(geometry, distance, precDigits); final GeometryFactory precisionModel = geometry.getGeometryFactory() .convertScales(sizeBasedScaleFactor, sizeBasedScaleFactor); return bufferFixedPrecision(precisionModel, geometry, distance, parameters); } catch (final TopologyException e) { saveException = e; // TODO remove // throw e; } } throw saveException; } /** * Completes the building of the input subgraphs by depth-labelling them, * and adds them to the PolygonBuilder. * The subgraph list must be sorted in rightmost-coordinate order. * * @param subgraphList the subgraphs to build * @param polyBuilder the PolygonBuilder which will build the final polygons */ private static void buildSubgraphs(final List<BufferSubgraph> subgraphList, final PolygonBuilder polyBuilder) { final List<BufferSubgraph> processedGraphs = new ArrayList<>(); for (final BufferSubgraph subgraph : subgraphList) { final Point p = subgraph.getRightmostCoordinate(); final int outsideDepth = getDepth(processedGraphs, p); subgraph.computeDepth(outsideDepth); subgraph.findResultEdges(); processedGraphs.add(subgraph); final List<DirectedEdge> edges = subgraph.getDirectedEdges(); final List<Node> nodes = subgraph.getNodes(); polyBuilder.add(edges, nodes); } } private static void computeNodedEdges(final Noder noder, final EdgeList edges, final List<NodedSegmentString> segments) { noder.computeNodes(segments); final Collection<NodedSegmentString> nodedSegments = noder.getNodedSubstrings(); for (final SegmentString segment : nodedSegments) { final int vertexCount = segment.size(); if (vertexCount > 2 || vertexCount == 2 && !segment.equalsVertex2d(0, 1)) { final Label oldLabel = (Label)segment.getData(); final Label label = new Label(oldLabel); final LineString points = segment.getPoints(); final Edge edge = new Edge(points, label); insertUniqueEdge(edges, edge); } } } /** * Compute the change in depth as an edge is crossed from R to L */ private static int depthDelta(final Label label) { final Location lLoc = label.getLocation(0, Position.LEFT); final Location rLoc = label.getLocation(0, Position.RIGHT); if (lLoc == Location.INTERIOR && rLoc == Location.EXTERIOR) { return 1; } else if (lLoc == Location.EXTERIOR && rLoc == Location.INTERIOR) { return -1; } return 0; } /** * Finds all non-horizontal segments intersecting the stabbing line. * The stabbing line is the ray to the right of stabbingRayLeftPt. * * @param stabbingRayLeftPt the left-hand origin of the stabbing line * @return a List of {@link DepthSegments} intersecting the stabbing line */ private static List<DepthSegment> findStabbedSegments(final Collection<BufferSubgraph> graphs, final Point stabbingRayLeftPt) { final List<DepthSegment> segments = new ArrayList<>(); for (final BufferSubgraph graph : graphs) { final BoundingBox env = graph.getEnvelope(); if (stabbingRayLeftPt.getY() >= env.getMinY() && stabbingRayLeftPt.getY() <= env.getMaxY()) { final List<DirectedEdge> edges = graph.getDirectedEdges(); for (final DirectedEdge edge : edges) { if (edge.isForward()) { findStabbedSegments(graphs, stabbingRayLeftPt, edge, segments); } } } } return segments; } /** * Finds all non-horizontal segments intersecting the stabbing line * in the input dirEdge. * The stabbing line is the ray to the right of stabbingRayLeftPt. * * @param stabbingRayLeftPt the left-hand origin of the stabbing line * @param stabbedSegments the current list of {@link DepthSegments} intersecting the stabbing line */ private static void findStabbedSegments(final Collection<BufferSubgraph> subgraphs, final Point stabbingRayLeftPt, final DirectedEdge dirEdge, final List<DepthSegment> stabbedSegments) { final Edge edge = dirEdge.getEdge(); for (int i = 0; i < edge.getVertexCount() - 1; i++) { final Point p1 = edge.getPoint(i); LineSegment seg = new LineSegmentDouble(p1, edge.getPoint(i + 1)); double y1 = seg.getY(0); double y2 = seg.getY(1); // ensure segment always points upwards if (y1 > y2) { seg = seg.reverse(); y1 = seg.getY(0); y2 = seg.getY(1); } final double x1 = seg.getX(0); final double x2 = seg.getX(1); // skip segment if it is left of the stabbing line final double maxx = Math.max(x1, x2); if (maxx < stabbingRayLeftPt.getX()) { continue; } // skip horizontal segments (there will be a non-horizontal one carrying // the same depth info if (seg.isHorizontal()) { continue; } // skip if segment is above or below stabbing line if (stabbingRayLeftPt.getY() < y1 || stabbingRayLeftPt.getY() > y2) { continue; } // skip if stabbing ray is right of the segment if (CGAlgorithmsDD.orientationIndex(seg.getP0(), seg.getP1(), stabbingRayLeftPt) == CGAlgorithms.RIGHT) { continue; } // stabbing line cuts this segment, so record it int depth = dirEdge.getDepth(Position.LEFT); // if segment direction was flipped, use RHS depth instead if (!seg.getP0().equals(p1)) { depth = dirEdge.getDepth(Position.RIGHT); } final DepthSegment ds = new DepthSegment(seg, depth); stabbedSegments.add(ds); } } private static int getDepth(final Collection<BufferSubgraph> subgraphs, final Point p) { final List<DepthSegment> stabbedSegments = findStabbedSegments(subgraphs, p); // if no segments on stabbing line subgraph must be outside all others. if (stabbedSegments.size() == 0) { return 0; } else { Collections.sort(stabbedSegments); final DepthSegment ds = stabbedSegments.get(0); return ds.getLeftDepth(); } } /** * Inserted edges are checked to see if an identical edge already exists. * If so, the edge is not inserted, but its label is merged * with the existing edge. */ private static void insertUniqueEdge(final EdgeList edgeList, final Edge edge) { // <FIX> MD 8 Oct 03 speed up identical edge lookup // fast lookup final Edge existingEdge = edgeList.findEqualEdge(edge); // If an identical edge already exists, simply update its label if (existingEdge != null) { final Label existingLabel = existingEdge.getLabel(); Label labelToMerge = edge.getLabel(); // check if new edge is in reverse direction to existing edge // if so, must flip the label before merging it if (!existingEdge.isPointwiseEqual(edge)) { labelToMerge = new Label(edge.getLabel()); labelToMerge.flip(); } existingLabel.merge(labelToMerge); // compute new depth delta of sum of edges final int mergeDelta = depthDelta(labelToMerge); final int existingDelta = existingEdge.getDepthDelta(); final int newDelta = existingDelta + mergeDelta; existingEdge.setDepthDelta(newDelta); } else { // no matching existing edge was found // add this new edge to the list of edges in this graph // e.setName(name + edges.size()); edgeList.add(edge); edge.setDepthDelta(depthDelta(edge.getLabel())); } } private static List<BufferSubgraph> newSubgraphs(final PlanarGraph graph) { final List<BufferSubgraph> subgraphList = new ArrayList<>(); for (final Node node : graph.getNodes()) { if (!node.isVisited()) { final BufferSubgraph subgraph = new BufferSubgraph(); subgraph.newNode(node); subgraphList.add(subgraph); } } /** * Sort the subgraphs in descending order of their rightmost coordinate. * This ensures that when the Polygons for the subgraphs are built, * subgraphs for shells will have been built before the subgraphs for * any holes they contain. */ Collections.sort(subgraphList, Collections.reverseOrder()); return subgraphList; } /** * Compute a scale factor to limit the precision of * a given combination of Geometry and buffer distance. * The scale factor is determined by * the number of digits of precision in the (geometry + buffer distance), * limited by the supplied <code>maxPrecisionDigits</code> value. * <p> * The scale factor is based on the absolute magnitude of the (geometry + buffer distance). * since this determines the number of digits of precision which must be handled. * * @param geometry the Geometry being buffered * @param distance the buffer distance * @param maxPrecisionDigits the max # of digits that should be allowed by * the precision determined by the computed scale factor * * @return a scale factor for the buffer computation */ private static double precisionScaleFactor(final Geometry geometry, final double distance, final int maxPrecisionDigits) { final BoundingBox boundingBox = geometry.getBoundingBox(); final double envMax = MathUtil.max(Math.abs(boundingBox.getMaxX()), Math.abs(boundingBox.getMaxY()), Math.abs(boundingBox.getMinX()), Math.abs(boundingBox.getMinY())); final double expandByDistance = distance > 0.0 ? distance : 0.0; final double bufEnvMax = envMax + 2 * expandByDistance; // the smallest power of 10 greater than the buffer envelope final int bufEnvPrecisionDigits = (int)(Math.log(bufEnvMax) / Math.log(10) + 1.0); final int minUnitLog10 = maxPrecisionDigits - bufEnvPrecisionDigits; final double scaleFactor = Math.pow(10.0, minUnitLog10); return scaleFactor; } }