/*
* 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.List;
import com.revolsys.geometry.geomgraph.Label;
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.LinearRing;
import com.revolsys.geometry.model.Location;
import com.revolsys.geometry.model.Point;
import com.revolsys.geometry.model.Polygon;
import com.revolsys.geometry.model.coordinates.LineSegmentUtil;
import com.revolsys.geometry.model.util.TriangleImpl;
import com.revolsys.geometry.noding.NodedSegmentString;
import com.revolsys.geometry.noding.SegmentString;
/**
* Creates all the raw offset curves for a buffer of a {@link Geometry}.
* Raw curves need to be noded together and polygonized to form the final buffer area.
*
* @version 1.7
*/
public class OffsetCurveSetBuilder {
private final OffsetCurveBuilder curveBuilder;
private final List<NodedSegmentString> curveList = new ArrayList<>();
private final double distance;
private final Geometry geometry;
public OffsetCurveSetBuilder(final Geometry inputGeom, final double distance,
final GeometryFactory precisionModel, final BufferParameters parameters) {
this.geometry = inputGeom;
this.distance = distance;
this.curveBuilder = new OffsetCurveBuilder(precisionModel, parameters);
}
public OffsetCurveSetBuilder(final Geometry inputGeom, final double distance,
final OffsetCurveBuilder curveBuilder) {
this.geometry = inputGeom;
this.distance = distance;
this.curveBuilder = curveBuilder;
}
private void add(final Geometry geometry) {
if (geometry.isEmpty()) {
return;
} else if (geometry instanceof Polygon) {
addPolygon((Polygon)geometry);
} else if (geometry instanceof LineString) {
addLineString((LineString)geometry);
} else if (geometry instanceof Point) {
addPoint((Point)geometry);
} else {
for (final Geometry part : geometry.geometries()) {
add(part);
}
}
}
/**
* Creates a {@link SegmentString} for a coordinate list which is a raw offset curve,
* and adds it to the list of buffer curves.
* The SegmentString is tagged with a Label giving the topology of the curve.
* The curve may be oriented in either direction.
* If the curve is oriented CW, the locations will be:
* <br>Left: Location.EXTERIOR
* <br>Right: Location.INTERIOR
*/
private void addCurve(final LineString points, final Location leftLoc, final Location rightLoc) {
if (points != null && points.getVertexCount() >= 2) {
final Label label = new Label(0, Location.BOUNDARY, leftLoc, rightLoc);
final NodedSegmentString segment = new NodedSegmentString(points, label);
this.curveList.add(segment);
}
}
private void addLineString(final LineString line) {
// a zero or negative width buffer of a line/point is empty
if (this.distance <= 0.0 && !this.curveBuilder.getBufferParameters().isSingleSided()) {
return;
} else {
final LineString points = line.removeDuplicatePoints();
final LineString curve = this.curveBuilder.getLineCurve(points, this.distance);
addCurve(curve, Location.EXTERIOR, Location.INTERIOR);
}
}
/**
* Add a Point to the graph.
*/
private void addPoint(final Point point) {
// a zero or negative width buffer of a line/point is empty
if (this.distance > 0.0) {
final LineString curve = this.curveBuilder.getPointCurve(point, this.distance);
addCurve(curve, Location.EXTERIOR, Location.INTERIOR);
}
}
private void addPolygon(final Polygon p) {
double offsetDistance = this.distance;
int offsetSide = Position.LEFT;
if (this.distance < 0.0) {
offsetDistance = -this.distance;
offsetSide = Position.RIGHT;
}
final LinearRing shell = p.getShell();
final boolean shellClockwise = shell.isClockwise();
final LinearRing shellCoord = shell.removeDuplicatePoints();
if (this.distance < 0.0 && isErodedCompletely(shell, this.distance)) {
// optimization - don't bother computing buffer
// if the polygon would be completely eroded
} else if (this.distance <= 0.0 && shellCoord.getVertexCount() < 3) {
// don't attempt to buffer a polygon with too few distinct vertices
} else {
addPolygonRing(shellCoord, shellClockwise, offsetDistance, offsetSide, Location.EXTERIOR,
Location.INTERIOR);
for (int i = 0; i < p.getHoleCount(); i++) {
final LinearRing hole = p.getHole(i);
final boolean holeClockwise = hole.isClockwise();
final LinearRing holeCoord = hole.removeDuplicatePoints();
// optimization - don't bother computing buffer for this hole
// if the hole would be completely covered
if (!(this.distance > 0.0 && isErodedCompletely(hole, -this.distance))) {
// Holes are topologically labeled opposite to the shell, since
// the interior of the polygon lies on their opposite side
// (on the left, if the hole is oriented CCW)
final int opposite = Position.opposite(offsetSide);
addPolygonRing(holeCoord, holeClockwise, offsetDistance, opposite, Location.INTERIOR,
Location.EXTERIOR);
}
}
}
}
/**
* Adds an offset curve for a polygon ring.
* The side and 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 and the side flipped.
*
* @param points the coordinates of the ring (must not contain repeated points)
* @param offsetDistance the distance at which to create the buffer
* @param side the side of the ring on which to construct the buffer line
* @param cwLeftLoc the location on the L side of the ring (if it is CW)
* @param cwRightLoc the location on the R side of the ring (if it is CW)
*/
private void addPolygonRing(final LineString points, final boolean clockwise,
final double offsetDistance, int side, final Location cwLeftLoc, final Location cwRightLoc) {
// don't bother adding ring if it is "flat" and will disappear in the output
if (offsetDistance == 0.0 && points.getVertexCount() < 4) {
return;
}
Location leftLoc = cwLeftLoc;
Location rightLoc = cwRightLoc;
if (points.getVertexCount() >= 4 && !clockwise) {
leftLoc = cwRightLoc;
rightLoc = cwLeftLoc;
side = Position.opposite(side);
}
final LineString curve = this.curveBuilder.getRingCurve(points, side, offsetDistance);
addCurve(curve, leftLoc, rightLoc);
}
/**
* Computes the set of raw offset curves for the buffer.
* Each offset curve has an attached {@link Label} indicating
* its left and right location.
*
* @return a Collection of SegmentStrings representing the raw buffer curves
*/
public List<NodedSegmentString> getCurves() {
add(this.geometry);
return this.curveList;
}
/**
* The ringCoord is assumed to contain no repeated points.
* It may be degenerate (i.e. contain only 1, 2, or 3 points).
* In this case it has no area, and hence has a minimum diameter of 0.
*
* @param ringCoord
* @param offsetDistance
* @return
*/
private boolean isErodedCompletely(final LinearRing ring, final double bufferDistance) {
// degenerate ring has no area
if (ring.getVertexCount() < 4) {
return bufferDistance < 0;
}
// important test to eliminate inverted triangle bug
// also optimizes erosion test for triangles
if (ring.getVertexCount() == 4) {
return isTriangleErodedCompletely(ring, bufferDistance);
}
// if envelope is narrower than twice the buffer distance, ring is eroded
final BoundingBox env = ring.getBoundingBox();
final double envMinDimension = Math.min(env.getHeight(), env.getWidth());
if (bufferDistance < 0.0 && 2 * Math.abs(bufferDistance) > envMinDimension) {
return true;
}
return false;
}
/**
* Tests whether a triangular ring would be eroded completely by the given
* buffer distance.
* This is a precise test. It uses the fact that the inner buffer of a
* triangle converges on the inCentre of the triangle (the point
* equidistant from all sides). If the buffer distance is greater than the
* distance of the inCentre from a side, the triangle will be eroded completely.
*
* This test is important, since it removes a problematic case where
* the buffer distance is slightly larger than the inCentre distance.
* In this case the triangle buffer curve "inverts" with incorrect topology,
* producing an incorrect hole in the buffer.
*
* @param triangleCoord
* @param bufferDistance
* @return
*/
private boolean isTriangleErodedCompletely(final LinearRing triangleCoord,
final double bufferDistance) {
final TriangleImpl tri = new TriangleImpl(triangleCoord.getVertex(0),
triangleCoord.getVertex(1), triangleCoord.getVertex(2));
final Point inCentre = tri.inCentre();
final double distToCentre = LineSegmentUtil.distanceLinePoint(tri.p0, tri.p1, inCentre);
return distToCentre < Math.abs(bufferDistance);
}
}