Skip to content

Commit

Permalink
Improve VariableBuffer segment buffer cap generation (#1041)
Browse files Browse the repository at this point in the history
  • Loading branch information
dr-jts authored Mar 15, 2024
1 parent 81639c5 commit 981c574
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.util.List;

import org.locationtech.jts.algorithm.Angle;
import org.locationtech.jts.algorithm.Orientation;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateList;
import org.locationtech.jts.geom.Geometry;
Expand All @@ -38,6 +39,8 @@
*/
public class VariableBuffer {

private static final int MIN_CAP_SEG_LEN_FACTOR = 4;

/**
* Creates a buffer polygon along a line with the buffer distance interpolated
* between a start distance and an end distance.
Expand Down Expand Up @@ -270,22 +273,28 @@ public Geometry getResult() {
private Polygon segmentBuffer(Coordinate p0, Coordinate p1,
double dist0, double dist1) {
/**
* Skip polygon if both distances are zero
* Skip buffer polygon if both distances are zero
*/
if (dist0 <= 0 && dist1 <= 0)
return null;

/**
* Compute for increasing distance only, so flip if needed
* Generation algorithm requires increasing distance, so flip if needed
*/
if (dist0 > dist1) {
return segmentBuffer(p1, p0, dist1, dist0);
return segmentBufferOriented(p1, p0, dist1, dist0);
}

// forward tangent line
return segmentBufferOriented(p0, p1, dist0, dist1);
}

private Polygon segmentBufferOriented(Coordinate p0, Coordinate p1,
double dist0, double dist1) {
//-- Assert: dist0 <= dist1

//-- forward tangent line
LineSegment tangent = outerTangent(p0, dist0, p1, dist1);

// if tangent is null then compute a buffer for largest circle
//-- if tangent is null then compute a buffer for largest circle
if (tangent == null) {
Coordinate center = p0;
double dist = dist0;
Expand All @@ -296,37 +305,32 @@ private Polygon segmentBuffer(Coordinate p0, Coordinate p1,
return circle(center, dist);
}

Coordinate t0 = tangent.getCoordinate(0);
Coordinate t1 = tangent.getCoordinate(1);

// reverse tangent line on other side of segment
LineSegment seg = new LineSegment(p0, p1);
Coordinate tr0 = seg.reflect(t0);
Coordinate tr1 = seg.reflect(t1);
//-- avoid numeric jitter if first distance is zero
if (dist0 == 0)
tr0 = p0.copy();
//-- reverse tangent line on other side of segment
LineSegment tangentReflect = reflect(tangent, p0, p1, dist0);

CoordinateList coords = new CoordinateList();
coords.add(t0, false);
coords.add(t1, false);

// end cap
addCap(p1, dist1, t1, tr1, coords);

coords.add(tr1, false);
coords.add(tr0, false);
//-- end cap
addCap(p1, dist1, tangent.p1, tangentReflect.p1, coords);
//-- start cap
addCap(p0, dist0, tangentReflect.p0, tangent.p0, coords);

// start cap
addCap(p0, dist0, tr0, t0, coords);

// close
coords.add(t0, false);
coords.closeRing();

Coordinate[] pts = coords.toCoordinateArray();
Polygon polygon = geomFactory.createPolygon(pts);
//System.out.println(polygon);
return polygon;
}

private LineSegment reflect(LineSegment seg, Coordinate p0, Coordinate p1, double dist0) {
LineSegment line = new LineSegment(p0, p1);
Coordinate r0 = line.reflect(seg.p0);
Coordinate r1 = line.reflect(seg.p1);
//-- avoid numeric jitter if first distance is zero (second dist must be > 0)
if (dist0 == 0)
r0 = p0.copy();
return new LineSegment(r0, r1);
}

/**
* Returns a circular polygon.
Expand All @@ -350,6 +354,10 @@ private Polygon circle(Coordinate center, double radius) {

/**
* Adds a semi-circular cap CCW around the point p.
* <>p>
* The vertices in caps are generated at fixed angles around a point.
* This allows caps at the same point to share vertices,
* which reduces artifacts when the segment buffers are merged.
*
* @param p the centre point of the cap
* @param r the cap radius
Expand All @@ -358,12 +366,14 @@ private Polygon circle(Coordinate center, double radius) {
* @param coords the coordinate list to add to
*/
private void addCap(Coordinate p, double r, Coordinate t1, Coordinate t2, CoordinateList coords) {
//-- handle zero-width at vertex
//-- if radius is zero just copy the vertex
if (r == 0) {
coords.add(p.copy(), false);
return;
}

coords.add(t1, false);

double angStart = Angle.angle(p, t1);
double angEnd = Angle.angle(p, t2);
if (angStart < angEnd)
Expand All @@ -372,18 +382,55 @@ private void addCap(Coordinate p, double r, Coordinate t1, Coordinate t2, Coordi
int indexStart = capAngleIndex(angStart);
int indexEnd = capAngleIndex(angEnd);

for (int i = indexStart; i > indexEnd; i--) {
// use negative increment to create points CW
double capSegLen = r * 2 * Math.sin(Math.PI / 4 / quadrantSegs);
double minSegLen = capSegLen / MIN_CAP_SEG_LEN_FACTOR;

for (int i = indexStart; i >= indexEnd; i--) {
//-- use negative increment to create points CW
double ang = capAngle(i);
coords.add( projectPolar(p, r, ang), false );
Coordinate capPt = projectPolar(p, r, ang);

boolean isCapPointHighQuality = true;
/**
* Due to the fixed locations of the cap points,
* a start or end cap point might create
* a "reversed" segment to the next tangent point.
* This causes an unwanted narrow spike in the buffer curve,
* which can cause holes in the final buffer polygon.
* These checks remove these points.
*/
if (i == indexStart
&& Orientation.CLOCKWISE != Orientation.index(p, t1, capPt)) {
isCapPointHighQuality = false;
}
else if (i == indexEnd
&& Orientation.COUNTERCLOCKWISE != Orientation.index(p, t2, capPt)) {
isCapPointHighQuality = false;
}

/**
* Remove short segments between the cap and the tangent segments.
*/
if (capPt.distance(t1) < minSegLen) {
isCapPointHighQuality = false;
}
else if (capPt.distance(t2) < minSegLen) {
isCapPointHighQuality = false;
}

if (isCapPointHighQuality) {
coords.add(capPt, false );
}
}

coords.add(t2, false);
}

/**
* Computes the angle for the given cap point index.
* Computes the actual angle for a cap angle index.
*
* @param index the fillet angle index
* @return
* @param index the cap angle index
* @return the angle
*/
private double capAngle(int index) {
double capSegAng = Math.PI / 2 / quadrantSegs;
Expand All @@ -392,15 +439,13 @@ private double capAngle(int index) {

/**
* Computes the canonical cap point index for a given angle.
* The angle is rounded down to the next lower
* index.
* The angle is rounded down to the next lower index.
* <p>
* In order to reduce the number of points created by overlapping end caps,
* cap points are generated at the same locations around a circle.
* The index is the index of the points around the circle,
* with 0 being the point at (1,0).
* The total number of points around the circle is
* <code>4 * quadrantSegs</code>.
* The total number of points around the circle is <code>4 * quadrantSegs</code>.
*
* @param ang the angle
* @return the index for the angle.
Expand All @@ -414,6 +459,7 @@ private int capAngleIndex(double ang) {
/**
* Computes the two circumference points defining the outer tangent line
* between two circles.
* The tangent line may be null if one circle mostly overlaps the other.
* <p>
* For the algorithm see <a href='https://en.wikipedia.org/wiki/Tangent_lines_to_circles#Outer_tangent'>Wikipedia</a>.
*
Expand Down
Loading

0 comments on commit 981c574

Please sign in to comment.