/*
* 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;
import com.revolsys.geometry.algorithm.CGAlgorithms;
import com.revolsys.geometry.algorithm.CGAlgorithmsDD;
import com.revolsys.geometry.algorithm.HCoordinate;
import com.revolsys.geometry.algorithm.LineIntersector;
import com.revolsys.geometry.algorithm.NotRepresentableException;
import com.revolsys.geometry.algorithm.RobustLineIntersector;
import com.revolsys.geometry.geomgraph.Position;
import com.revolsys.geometry.model.GeometryFactory;
import com.revolsys.geometry.model.LineJoin;
import com.revolsys.geometry.model.LineString;
import com.revolsys.geometry.model.Point;
import com.revolsys.geometry.model.segment.LineSegment;
import com.revolsys.geometry.model.segment.LineSegmentDouble;
import com.revolsys.math.Angle;
import com.revolsys.util.MathUtil;
/**
* Generates segments which form an offset curve.
* Supports all end cap and join options
* provided for buffering.
* This algorithm implements various heuristics to
* produce smoother, simpler curves which are
* still within a reasonable tolerance of the
* true curve.
*
* @author Martin Davis
*
*/
class OffsetSegmentGenerator {
/**
* Factor which controls how close curve vertices can be to be snapped
*/
private static final double CURVE_VERTEX_SNAP_DISTANCE_FACTOR = 1.0E-6;
/**
* Factor which controls how close curve vertices on inside turns can be to be snapped
*/
private static final double INSIDE_TURN_VERTEX_SNAP_DISTANCE_FACTOR = 1.0E-3;
/**
* Factor which determines how short closing segs can be for round buffers
*/
private static final int MAX_CLOSING_SEG_LEN_FACTOR = 80;
/**
* Factor which controls how close offset segments can be to
* skip adding a filler or mitre.
*/
private static final double OFFSET_SEGMENT_SEPARATION_FACTOR = 1.0E-3;
private final BufferParameters bufParams;
/**
* The Closing Segment Length Factor controls how long
* "closing segments" are. Closing segments are added
* at the middle of inside corners to ensure a smoother
* boundary for the buffer offset curve.
* In some cases (particularly for round joins with default-or-better
* quantization) the closing segments can be made quite short.
* This substantially improves performance (due to fewer intersections being created).
*
* A closingSegFactor of 0 results in lines to the corner vertex
* A closingSegFactor of 1 results in lines halfway to the corner vertex
* A closingSegFactor of 80 results in lines 1/81 of the way to the corner vertex
* (this option is reasonable for the very common default situation of round joins
* and quadrantSegs >= 8)
*/
private int closingSegLengthFactor = 1;
private double distance = 0.0;
/**
* The angle quantum with which to approximate a fillet curve
* (based on the input # of quadrant segments)
*/
private final double filletAngleQuantum;
private boolean hasNarrowConcaveAngle = false;
private final LineIntersector li;
private LineSegment offset0;
private LineSegment offset1;
private final GeometryFactory geometryFactory;
private double s0X;
private double s0Y;
private double s1X;
private double s1Y;
private double s2X;
private double s2Y;
private final OffsetSegmentString segList;
private int side = 0;
public OffsetSegmentGenerator(final GeometryFactory geometryFactory,
final BufferParameters bufParams, final double distance) {
this.geometryFactory = geometryFactory;
this.bufParams = bufParams;
// compute intersections in full precision, to provide accuracy
// the points are rounded as they are inserted into the curve line
this.li = new RobustLineIntersector();
this.filletAngleQuantum = Math.PI / 2.0 / bufParams.getQuadrantSegments();
/**
* Non-round joins cause issues with short closing segments, so don't use
* them. In any case, non-round joins only really make sense for relatively
* small buffer distances.
*/
if (bufParams.getQuadrantSegments() >= 8 && bufParams.getJoinStyle() == LineJoin.ROUND) {
this.closingSegLengthFactor = MAX_CLOSING_SEG_LEN_FACTOR;
}
this.distance = distance;
// Choose the min vertex separation as a small fraction of the offset distance.
final double minimimVertexDistance = distance * CURVE_VERTEX_SNAP_DISTANCE_FACTOR;
this.segList = new OffsetSegmentString(this.geometryFactory, minimimVertexDistance);
}
/**
* Adds a bevel join connecting the two offset segments
* around a reflex corner.
*
* @param offset0 the first offset segment
* @param offset1 the second offset segment
*/
private void addBevelJoin(final LineSegment offset0, final LineSegment offset1) {
final double x01 = offset0.getX(1);
final double y01 = offset0.getY(1);
this.segList.addPoint(x01, y01);
final double x10 = offset1.getX(0);
final double y10 = offset1.getY(0);
this.segList.addPoint(x10, y10);
}
private void addCollinear(final boolean addStartPoint) {
/**
* This test could probably be done more efficiently,
* but the situation of exact collinearity should be fairly rare.
*/
this.li.computeIntersection(this.s0X, this.s0Y, this.s1X, this.s1Y, this.s1X, this.s1Y,
this.s2X, this.s2Y);
final int intersectionCount = this.li.getIntersectionCount();
/**
* if numInt is < 2, the lines are parallel and in the same direction. In
* this case the point can be ignored, since the offset lines will also be
* parallel.
*/
if (intersectionCount >= 2) {
final double x01 = this.offset0.getX(1);
final double y01 = this.offset0.getY(1);
final double x10 = this.offset1.getX(0);
final double y10 = this.offset1.getY(0);
/**
* segments are collinear but reversing.
* Add an "end-cap" fillet
* all the way around to other direction This case should ONLY happen
* for LineStrings, so the orientation is always CW. (Polygons can never
* have two consecutive segments which are parallel but reversed,
* because that would be a self intersection.
*
*/
if (this.bufParams.getJoinStyle() == LineJoin.BEVEL
|| this.bufParams.getJoinStyle() == LineJoin.MITER) {
if (addStartPoint) {
this.segList.addPoint(x01, y01);
}
this.segList.addPoint(x10, y10);
} else {
addFillet(this.s1X, this.s1Y, x01, y01, x10, y10, CGAlgorithms.CLOCKWISE, this.distance);
}
}
}
private void addFillet(final double x, final double y, final double x1, final double y1,
final double x2, final double y2, final int direction, final double radius) {
final double dx0 = x1 - x;
final double dy0 = y1 - y;
double startAngle = Math.atan2(dy0, dx0);
final double dx1 = x2 - x;
final double dy1 = y2 - y;
final double endAngle = Math.atan2(dy1, dx1);
if (direction == CGAlgorithms.CLOCKWISE) {
if (startAngle <= endAngle) {
startAngle += 2.0 * Math.PI;
}
} else { // direction == COUNTERCLOCKWISE
if (startAngle >= endAngle) {
startAngle -= 2.0 * Math.PI;
}
}
this.segList.addPoint(x1, y1);
addFillet(x, y, startAngle, endAngle, direction, radius);
this.segList.addPoint(x2, y2);
}
/**
* Adds points for a circular fillet arc
* between two specified angles.
* The start and end point for the fillet are not added -
* the caller must add them if required.
*
* @param direction is -1 for a CW angle, 1 for a CCW angle
* @param radius the radius of the fillet
*/
private void addFillet(final double x, final double y, final double startAngle,
final double endAngle, final int direction, final double radius) {
final int directionFactor = direction == CGAlgorithms.CLOCKWISE ? -1 : 1;
final double totalAngle = Math.abs(startAngle - endAngle);
final int segmentCount = (int)(totalAngle / this.filletAngleQuantum + 0.5);
if (segmentCount > 0) {
// choose angle increment so that each segment has equal length
final double initAngle = 0.0;
final double currAngleInc = totalAngle / segmentCount;
double currAngle = initAngle;
while (currAngle < totalAngle) {
final double angle = startAngle + directionFactor * currAngle;
final double newX = x + radius * Math.cos(angle);
final double newY = y + radius * Math.sin(angle);
this.segList.addPoint(newX, newY);
currAngle += currAngleInc;
}
}
}
public void addFirstSegment() {
final double x10 = this.offset1.getX(0);
final double y10 = this.offset1.getY(0);
this.segList.addPoint(x10, y10);
}
/**
* Adds the offset points for an inside (concave) turn.
*
* @param orientation
* @param addStartPoint
*/
private void addInsideTurn(final int orientation, final boolean addStartPoint) {
final double x00 = this.offset0.getX(0);
final double y00 = this.offset0.getY(0);
final double x01 = this.offset0.getX(1);
final double y01 = this.offset0.getY(1);
final double x10 = this.offset1.getX(0);
final double y10 = this.offset1.getY(0);
final double x11 = this.offset1.getX(1);
final double y11 = this.offset1.getY(1);
/**
* add intersection point of offset segments (if any)
*/
this.li.computeIntersection(x00, y00, x01, y01, x10, y10, x11, y11);
if (this.li.hasIntersection()) {
final Point intersection = this.li.getIntersection(0);
final double intersectionX = intersection.getX();
final double intersectionY = intersection.getY();
this.segList.addPoint(intersectionX, intersectionY);
} else {
/**
* If no intersection is detected,
* it means the angle is so small and/or the offset so
* large that the offsets segments don't intersect.
* In this case we must
* add a "closing segment" to make sure the buffer curve is continuous,
* fairly smooth (e.g. no sharp reversals in direction)
* and tracks the buffer correctly around the corner. The curve connects
* the endpoints of the segment offsets to points
* which lie toward the centre point of the corner.
* The joining curve will not appear in the final buffer outline, since it
* is completely internal to the buffer polygon.
*
* In complex buffer cases the closing segment may cut across many other
* segments in the generated offset curve. In order to improve the
* performance of the noding, the closing segment should be kept as short as possible.
* (But not too short, since that would defeat its purpose).
* This is the purpose of the closingSegFactor heuristic value.
*/
/**
* The intersection test above is vulnerable to robustness errors; i.e. it
* may be that the offsets should intersect very close to their endpoints,
* but aren't reported as such due to rounding. To handle this situation
* appropriately, we use the following test: If the offset points are very
* close, don't add closing segments but simply use one of the offset
* points
*/
this.hasNarrowConcaveAngle = true;
if (MathUtil.distance(x01, y01, x10, y10) < this.distance
* INSIDE_TURN_VERTEX_SNAP_DISTANCE_FACTOR) {
this.segList.addPoint(x01, y01);
} else {
// add endpoint of this segment offset
this.segList.addPoint(x01, y01);
/**
* Add "closing segment" of required length.
*/
if (this.closingSegLengthFactor > 0) {
final double midX0 = (this.closingSegLengthFactor * x01 + this.s1X)
/ (this.closingSegLengthFactor + 1);
final double midY0 = (this.closingSegLengthFactor * y01 + this.s1Y)
/ (this.closingSegLengthFactor + 1);
this.segList.addPoint(midX0, midY0);
final double midX1 = (this.closingSegLengthFactor * x10 + this.s1X)
/ (this.closingSegLengthFactor + 1);
final double midY1 = (this.closingSegLengthFactor * y10 + this.s1Y)
/ (this.closingSegLengthFactor + 1);
this.segList.addPoint(midX1, midY1);
} else {
/**
* This branch is not expected to be used except for testing purposes.
* It is equivalent to the JTS 1.9 logic for closing segments
* (which results in very poor performance for large buffer distances)
*/
this.segList.addPoint(this.s1X, this.s1Y);
}
// */
// add start point of next segment offset
this.segList.addPoint(x10, y10);
}
}
}
/**
* Add last offset point
*/
public void addLastSegment() {
final double x11 = this.offset1.getX(1);
final double y11 = this.offset1.getY(1);
this.segList.addPoint(x11, y11);
}
/**
* Adds a limited mitre join connecting the two reflex offset segments.
* A limited mitre is a mitre which is beveled at the distance
* determined by the mitre ratio limit.
*
* @param offset0 the first offset segment
* @param offset1 the second offset segment
* @param distance the offset distance
* @param mitreLimit the mitre limit ratio
*/
private void addLimitedMitreJoin(final LineSegment offset0, final LineSegment offset1,
final double distance, final double mitreLimit) {
final double basePtX = this.s1X;
final double basePtY = this.s1Y;
final double ang0 = Angle.angle2d(basePtX, basePtY, this.s0X, this.s0Y);
final double ang2 = Angle.angle2d(basePtX, basePtY, this.s2X, this.s2Y);
// oriented angle between segments
final double angDiff = Angle.angleBetweenOriented(ang0, ang2);
// half of the interior angle
final double angDiffHalf = angDiff / 2;
// angle for bisector of the interior angle between the segments
final double midAng = Angle.normalize(ang0 + angDiffHalf);
// rotating this by PI gives the bisector of the reflex angle
final double mitreMidAng = Angle.normalize(midAng + Math.PI);
// the miterLimit determines the distance to the mitre bevel
final double mitreDist = mitreLimit * distance;
// the bevel delta is the difference between the buffer distance
// and half of the length of the bevel segment
final double bevelDelta = mitreDist * Math.abs(Math.sin(angDiffHalf));
final double bevelHalfLen = distance - bevelDelta;
// compute the midpoint of the bevel segment
final double bevelMidX = basePtX + mitreDist * Math.cos(mitreMidAng);
final double bevelMidY = basePtY + mitreDist * Math.sin(mitreMidAng);
// compute the mitre midline segment from the corner point to the bevel
// segment midpoint
final LineSegment mitreMidLine = new LineSegmentDouble(2, basePtX, basePtY, bevelMidX,
bevelMidY);
// finally the bevel segment endpoints are computed as offsets from
// the mitre midline
final Point bevelEndLeft = mitreMidLine.pointAlongOffset(1.0, bevelHalfLen);
final Point bevelEndRight = mitreMidLine.pointAlongOffset(1.0, -bevelHalfLen);
if (this.side == Position.LEFT) {
this.segList.addPoint(bevelEndLeft);
this.segList.addPoint(bevelEndRight);
} else {
this.segList.addPoint(bevelEndRight);
this.segList.addPoint(bevelEndLeft);
}
}
/**
* Add an end cap around point p1, terminating a line segment coming from p0
*/
public void addLineEndCap(final double x1, final double y1, final double x2, final double y2) {
final LineSegment offsetL = newOffsetSegment(x1, y1, x2, y2, Position.LEFT, this.distance);
final LineSegment offsetR = newOffsetSegment(x1, y1, x2, y2, Position.RIGHT, this.distance);
final double dx = x2 - x1;
final double dy = y2 - y1;
final double angle = Math.atan2(dy, dx);
final double leftX2 = offsetL.getX(1);
final double leftY2 = offsetL.getY(1);
final double rightX2 = offsetR.getX(1);
final double rightY2 = offsetR.getY(1);
switch (this.bufParams.getEndCapStyle()) {
case ROUND:
// add offset seg points with a fillet between them
this.segList.addPoint(leftX2, leftY2);
addFillet(x2, y2, angle + Math.PI / 2, angle - Math.PI / 2, CGAlgorithms.CLOCKWISE,
this.distance);
this.segList.addPoint(rightX2, rightY2);
break;
case BUTT:
// only offset segment points are added
this.segList.addPoint(leftX2, leftY2);
this.segList.addPoint(rightX2, rightY2);
break;
case SQUARE:
final double absDistance = Math.abs(this.distance);
// add a square defined by extensions of the offset segment endpoints
final double squareCapSideOffsetX = absDistance * Math.cos(angle);
final double squareCapSideOffsetY = absDistance * Math.sin(angle);
final double lx = leftX2 + squareCapSideOffsetX;
final double ly = leftY2 + squareCapSideOffsetY;
this.segList.addPoint(lx, ly);
final double rx = rightX2 + squareCapSideOffsetX;
final double ry = rightY2 + squareCapSideOffsetY;
this.segList.addPoint(rx, ry);
break;
}
}
/**
* Adds a mitre join connecting the two reflex offset segments.
* The mitre will be beveled if it exceeds the mitre ratio limit.
*
* @param offset0 the first offset segment
* @param offset1 the second offset segment
* @param distance the offset distance
*/
private void addMitreJoin(final double x, final double y, final LineSegment offset0,
final LineSegment offset1, final double distance) {
boolean isMitreWithinLimit = true;
double intPtX = 0;
double intPtY = 0;
/**
* This computation is unstable if the offset segments are nearly collinear.
* Howver, this situation should have been eliminated earlier by the check for
* whether the offset segment endpoints are almost coincident
*/
try {
final double line1x1 = offset0.getX(0);
final double line1y1 = offset0.getY(0);
final double line1x2 = offset0.getX(1);
final double line1y2 = offset0.getY(1);
final double line2x1 = offset1.getX(0);
final double line2y1 = offset1.getY(0);
final double line2x2 = offset1.getX(1);
final double line2y2 = offset1.getY(1);
final Point intersection = HCoordinate.intersection(line1x1, line1y1, line1x2, line1y2,
line2x1, line2y1, line2x2, line2y2);
intPtX = intersection.getX();
intPtY = intersection.getY();
final double mitreRatio;
if (distance <= 0.0) {
mitreRatio = 1;
} else {
mitreRatio = MathUtil.distance(intPtX, intPtY, x, y) / Math.abs(distance);
}
if (mitreRatio > this.bufParams.getMitreLimit()) {
isMitreWithinLimit = false;
}
} catch (final NotRepresentableException ex) {
isMitreWithinLimit = false;
}
if (isMitreWithinLimit) {
this.segList.addPoint(intPtX, intPtY);
} else {
addLimitedMitreJoin(offset0, offset1, distance, this.bufParams.getMitreLimit());
}
}
public void addNextSegment(final double x, final double y, final boolean addStartPoint) {
// s0-s1-s2 are the coordinates of the previous segment and the current one
this.s0X = this.s1X;
this.s0Y = this.s1Y;
this.s1X = this.s2X;
this.s1Y = this.s2Y;
this.s2X = x;
this.s2Y = y;
this.offset0 = newOffsetSegment(this.s0X, this.s0Y, this.s1X, this.s1Y, this.side,
this.distance);
this.offset1 = newOffsetSegment(this.s1X, this.s1Y, this.s2X, this.s2Y, this.side,
this.distance);
// do nothing if points are equal
if (this.s1X != this.s2X || this.s1Y != this.s2Y) {
final int orientation = CGAlgorithmsDD.orientationIndex(this.s0X, this.s0Y, this.s1X,
this.s1Y, this.s2X, this.s2Y);
final boolean outsideTurn = orientation == CGAlgorithms.CLOCKWISE
&& this.side == Position.LEFT
|| orientation == CGAlgorithms.COUNTERCLOCKWISE && this.side == Position.RIGHT;
if (orientation == 0) { // lines are collinear
addCollinear(addStartPoint);
} else if (outsideTurn) {
addOutsideTurn(orientation, addStartPoint);
} else { // inside turn
addInsideTurn(orientation, addStartPoint);
}
}
}
/**
* Adds the offset points for an outside (convex) turn
*
* @param orientation
* @param addStartPoint
*/
private void addOutsideTurn(final int orientation, final boolean addStartPoint) {
final double x01 = this.offset0.getX(1);
final double y01 = this.offset0.getY(1);
final double x10 = this.offset1.getX(0);
final double y10 = this.offset1.getY(0);
/**
* Heuristic: If offset endpoints are very close together,
* just use one of them as the corner vertex.
* This avoids problems with computing mitre corners in the case
* where the two segments are almost parallel
* (which is hard to compute a robust intersection for).
*/
if (MathUtil.distance(x01, y01, x10, y10) < this.distance * OFFSET_SEGMENT_SEPARATION_FACTOR) {
this.segList.addPoint(x01, y01);
return;
}
if (this.bufParams.getJoinStyle() == LineJoin.MITER) {
addMitreJoin(this.s1X, this.s1Y, this.offset0, this.offset1, this.distance);
} else if (this.bufParams.getJoinStyle() == LineJoin.BEVEL) {
addBevelJoin(this.offset0, this.offset1);
} else {
// add a circular fillet connecting the endpoints of the offset segments
if (addStartPoint) {
this.segList.addPoint(x01, y01);
}
// TESTING - comment out to produce beveled joins
addFillet(this.s1X, this.s1Y, x01, y01, x10, y10, orientation, this.distance);
this.segList.addPoint(x10, y10);
}
}
public void addSegments(final LineString points, final boolean isForward) {
this.segList.addPoints(points, isForward);
}
public void closeRing() {
this.segList.closeRing();
}
public LineString getPoints() {
return this.segList.getPoints();
}
/**
* Tests whether the input has a narrow concave angle
* (relative to the offset distance).
* In this case the generated offset curve will contain self-intersections
* and heuristic closing segments.
* This is expected behaviour in the case of Buffer curves.
* For pure Offset Curves,
* the output needs to be further treated
* before it can be used.
*
* @return true if the input has a narrow concave angle
*/
public boolean hasNarrowConcaveAngle() {
return this.hasNarrowConcaveAngle;
}
public void initSideSegments(final double s1X, final double s1Y, final double s2X,
final double s2Y, final int side) {
this.s1X = s1X;
this.s1Y = s1Y;
this.s2X = s2X;
this.s2Y = s2Y;
this.side = side;
this.offset1 = newOffsetSegment(this.s1X, this.s1Y, this.s2X, this.s2Y, side, this.distance);
}
/**
* Creates a CW circle around a point
*/
public void newCircle(final double x, final double y) {
// add start point
final double newX = x + this.distance;
this.segList.addPoint(newX, y);
addFillet(x, y, 0.0, 2.0 * Math.PI, -1, this.distance);
this.segList.closeRing();
}
/**
* Compute an offset segment for an input segment on a given side and at a given distance.
* The offset points are computed in full double precision, for accuracy.
*
* @param side the side of the segment ({@link Position}) the offset lies on
* @param distance the offset distance
* @param offset the points computed for the offset segment
*/
public LineSegment newOffsetSegment(final double x1, final double y1, final double x2,
final double y2, final int side, final double distance) {
final int sideSign;
if (side == Position.LEFT) {
sideSign = 1;
} else {
sideSign = -1;
}
final double dx = x2 - x1;
final double dy = y2 - y1;
final double len = Math.sqrt(dx * dx + dy * dy);
// u is the vector that is the length of the offset, in the direction of the
// segment
final double ux = sideSign * distance * dx / len;
final double uy = sideSign * distance * dy / len;
final double newX1 = this.geometryFactory.makePrecise(0, x1 - uy);
final double newY1 = this.geometryFactory.makePrecise(1, y1 + ux);
final double newX2 = this.geometryFactory.makePrecise(0, x2 - uy);
final double newY2 = this.geometryFactory.makePrecise(1, y2 + ux);
return new LineSegmentDouble(2, newX1, newY1, newX2, newY2);
}
/**
* Creates a CW square around a point
*/
public void newSquare(final double x, final double y) {
this.segList.addPoint(x + this.distance, y + this.distance);
this.segList.addPoint(x + this.distance, y - this.distance);
this.segList.addPoint(x - this.distance, y - this.distance);
this.segList.addPoint(x - this.distance, y + this.distance);
this.segList.closeRing();
}
@Override
public String toString() {
return this.segList.toString();
}
}