Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve VariableBuffer segment buffer cap generation #1041

Merged
merged 1 commit into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading