/* * 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.geomgraph.Position; import com.revolsys.geometry.model.Geometry; import com.revolsys.geometry.model.GeometryFactory; import com.revolsys.geometry.model.LineString; import com.revolsys.geometry.model.Point; /** * Computes the raw offset curve for a * single {@link Geometry} component (ring, line or point). * A raw offset curve line is not noded - * it may contain self-intersections (and usually will). * The final buffer polygon is computed by forming a topological graph * of all the noded raw curves and tracing outside contours. * The points in the raw curve are rounded * to a given scale. * * @version 1.7 */ public class OffsetCurveBuilder { /** * Use a value which results in a potential distance error which is * significantly less than the error due to * the quadrant segment discretization. * For QS = 8 a value of 100 is reasonable. * This should produce a maximum of 1% distance error. */ private static final double SIMPLIFY_FACTOR = 100.0; /** * Computes the distance tolerance to use during input * line simplification. * * @param distance the buffer distance * @return the simplification tolerance */ private static double simplifyTolerance(final double bufDistance) { return bufDistance / SIMPLIFY_FACTOR; } private final BufferParameters bufParams; private double distance = 0.0; private final GeometryFactory precisionModel; public OffsetCurveBuilder(final GeometryFactory precisionModel, final BufferParameters bufParams) { this.precisionModel = precisionModel; this.bufParams = bufParams; } private void computeLineBufferCurve(final LineString points, final OffsetSegmentGenerator segGen) { final double distTol = simplifyTolerance(this.distance); // --------- compute points for left side of line // Simplify the appropriate side of the line before generating final LineString simp1 = BufferInputLineSimplifier.simplify(points, distTol); // MD - used for testing only (to eliminate simplification) // Point[] simp1 = inputPts; final int n1 = simp1.getVertexCount() - 1; final double x1 = simp1.getX(0); final double y1 = simp1.getY(0); final double x2 = simp1.getX(1); final double y2 = simp1.getY(1); segGen.initSideSegments(x1, y1, x2, y2, Position.LEFT); for (int i = 2; i <= n1; i++) { final double x = simp1.getX(i); final double y = simp1.getY(i); segGen.addNextSegment(x, y, true); } segGen.addLastSegment(); // add line cap for end of line segGen.addLineEndCap(simp1.getX(n1 - 1), simp1.getY(n1 - 1), simp1.getX(n1), simp1.getY(n1)); // ---------- compute points for right side of line // Simplify the appropriate side of the line before generating final LineString simp2 = BufferInputLineSimplifier.simplify(points, -distTol); // MD - used for testing only (to eliminate simplification) // Point[] simp2 = inputPts; final int n2 = simp2.getVertexCount() - 1; // since we are traversing line in opposite order, offset position is still // LEFT segGen.initSideSegments(simp2.getX(n2), simp2.getY(n2), simp2.getX(n2 - 1), simp2.getY(n2 - 1), Position.LEFT); for (int i = n2 - 2; i >= 0; i--) { final double x = simp2.getX(i); final double y = simp2.getY(i); segGen.addNextSegment(x, y, true); } segGen.addLastSegment(); // add line cap for start of line segGen.addLineEndCap(simp2.getX(1), simp2.getY(1), simp2.getX(0), simp2.getY(0)); segGen.closeRing(); } private void computeOffsetCurve(final LineString points, final boolean isRightSide, final OffsetSegmentGenerator segGen) { final double distTol = simplifyTolerance(this.distance); if (isRightSide) { // ---------- compute points for right side of line // Simplify the appropriate side of the line before generating final LineString simp2 = BufferInputLineSimplifier.simplify(points, -distTol); // MD - used for testing only (to eliminate simplification) // Point[] simp2 = inputPts; final int n2 = simp2.getVertexCount() - 1; // since we are traversing line in opposite order, offset position is // still LEFT segGen.initSideSegments(simp2.getX(n2), simp2.getY(n2), simp2.getX(n2 - 1), simp2.getY(n2 - 1), Position.LEFT); segGen.addFirstSegment(); for (int i = n2 - 2; i >= 0; i--) { final double x = simp2.getX(i); final double y = simp2.getY(i); segGen.addNextSegment(x, y, true); } } else { // --------- compute points for left side of line // Simplify the appropriate side of the line before generating final LineString simp1 = BufferInputLineSimplifier.simplify(points, distTol); // MD - used for testing only (to eliminate simplification) // Point[] simp1 = inputPts; final int n1 = simp1.getVertexCount() - 1; segGen.initSideSegments(simp1.getX(0), simp1.getY(0), simp1.getX(1), simp1.getY(1), Position.LEFT); segGen.addFirstSegment(); for (int i = 2; i <= n1; i++) { final double x = simp1.getX(i); final double y = simp1.getY(i); segGen.addNextSegment(x, y, true); } } segGen.addLastSegment(); } private void computePointCurve(final double x, final double y, final OffsetSegmentGenerator segGen) { switch (this.bufParams.getEndCapStyle()) { case ROUND: segGen.newCircle(x, y); break; case SQUARE: segGen.newSquare(x, y); break; case BUTT: break; // otherwise curve is empty (e.g. for a butt cap); } } private void computeRingBufferCurve(final LineString inputLine, final int side, final OffsetSegmentGenerator segGen) { // simplify input line to improve performance double distTol = simplifyTolerance(this.distance); // ensure that correct side is simplified if (side == Position.RIGHT) { distTol = -distTol; } final LineString simplifiedLine = BufferInputLineSimplifier.simplify(inputLine, distTol); final int n = simplifiedLine.getVertexCount() - 1; segGen.initSideSegments(simplifiedLine.getX(n - 1), simplifiedLine.getY(n - 1), simplifiedLine.getX(0), simplifiedLine.getY(0), side); boolean addStartPoint = false; for (int vertexIndex = 1; vertexIndex <= n; vertexIndex++) { final double x = simplifiedLine.getX(vertexIndex); final double y = simplifiedLine.getY(vertexIndex); segGen.addNextSegment(x, y, addStartPoint); addStartPoint = true; } segGen.closeRing(); } private void computeSingleSidedBufferCurve(final LineString inputPts, final boolean isRightSide, final OffsetSegmentGenerator segGen) { final double distTol = simplifyTolerance(this.distance); if (isRightSide) { // add original line segGen.addSegments(inputPts, true); // ---------- compute points for right side of line // Simplify the appropriate side of the line before generating final LineString simp2 = BufferInputLineSimplifier.simplify(inputPts, -distTol); // MD - used for testing only (to eliminate simplification) // Point[] simp2 = inputPts; final int n2 = simp2.getVertexCount() - 1; // since we are traversing line in opposite order, offset position is // still LEFT segGen.initSideSegments(simp2.getX(n2), simp2.getY(n2), simp2.getX(n2 - 1), simp2.getY(n2 - 1), Position.LEFT); segGen.addFirstSegment(); for (int i = n2 - 2; i >= 0; i--) { final double x = simp2.getX(i); final double y = simp2.getY(i); segGen.addNextSegment(x, y, true); } } else { // add original line segGen.addSegments(inputPts, false); // --------- compute points for left side of line // Simplify the appropriate side of the line before generating final LineString simp1 = BufferInputLineSimplifier.simplify(inputPts, distTol); // MD - used for testing only (to eliminate simplification) // Point[] simp1 = inputPts; final int n1 = simp1.getVertexCount() - 1; segGen.initSideSegments(simp1.getX(0), simp1.getY(0), simp1.getX(1), simp1.getY(1), Position.LEFT); segGen.addFirstSegment(); for (int i = 2; i <= n1; i++) { final double x = simp1.getX(i); final double y = simp1.getY(i); segGen.addNextSegment(x, y, true); } } segGen.addLastSegment(); segGen.closeRing(); } /** * Gets the buffer parameters being used to generate the curve. * * @return the buffer parameters being used */ public BufferParameters getBufferParameters() { return this.bufParams; } /** * This method handles single points as well as LineStrings. * LineStrings are assumed <b>not</b> to be closed (the function will not * fail for closed lines, but will generate superfluous line caps). * * @param inputPts the vertices of the line to offset * @param distance the offset distance * * @return a Point array representing the curve * or null if the curve is empty */ public LineString getLineCurve(final LineString inputPts, final double distance) { this.distance = distance; // a zero or negative width buffer of a line/point is empty if (distance < 0.0 && !this.bufParams.isSingleSided()) { return null; } if (distance == 0.0) { return null; } final double posDistance = Math.abs(distance); final OffsetSegmentGenerator segGen = getSegGen(posDistance); final int vertexCount = inputPts.getVertexCount(); if (vertexCount == 0) { } else if (vertexCount == 1) { final double x = inputPts.getX(0); final double y = inputPts.getY(0); computePointCurve(x, y, segGen); } else { if (this.bufParams.isSingleSided()) { final boolean isRightSide = distance < 0.0; computeSingleSidedBufferCurve(inputPts, isRightSide, segGen); } else { computeLineBufferCurve(inputPts, segGen); } } return segGen.getPoints(); } public LineString getOffsetCurve(final LineString inputPts, final double distance) { this.distance = distance; // a zero width offset curve is empty if (distance == 0.0) { return null; } final boolean isRightSide = distance < 0.0; final double posDistance = Math.abs(distance); final OffsetSegmentGenerator segGen = getSegGen(posDistance); final int vertexCount = inputPts.getVertexCount(); if (vertexCount == 0) { } else if (vertexCount == 1) { final double x = inputPts.getX(0); final double y = inputPts.getY(0); computePointCurve(x, y, segGen); } else { computeOffsetCurve(inputPts, isRightSide, segGen); } final LineString curvePts = segGen.getPoints(); // for right side line is traversed in reverse direction, so have to reverse // generated line if (isRightSide) { curvePts.reverse(); } return curvePts; } public LineString getPointCurve(final Point point, final double distance) { this.distance = distance; // a zero or negative width buffer of a line/point is empty if (distance < 0.0 && !this.bufParams.isSingleSided()) { return null; } else if (distance == 0.0) { return null; } else { final double posDistance = Math.abs(distance); final OffsetSegmentGenerator segGen = getSegGen(posDistance); final double x = point.getX(); final double y = point.getY(); computePointCurve(x, y, segGen); return segGen.getPoints(); } } /** * This method handles the degenerate cases of single points and lines, * as well as rings. * * @return a Point array representing the curve * or null if the curve is empty */ public LineString getRingCurve(final LineString points, final int side, final double distance) { this.distance = distance; if (points.getVertexCount() <= 2) { return getLineCurve(points, distance); } // optimize creating ring for for zero distance if (distance == 0.0) { return points.clone(); } else { final OffsetSegmentGenerator segGen = getSegGen(distance); computeRingBufferCurve(points, side, segGen); return segGen.getPoints(); } } private OffsetSegmentGenerator getSegGen(final double distance) { return new OffsetSegmentGenerator(this.precisionModel, this.bufParams, distance); } }