Skip to content

Commit

Permalink
Fix buffer inverted ring removal heuristic (#1038)
Browse files Browse the repository at this point in the history
  • Loading branch information
dr-jts committed Mar 14, 2024
1 parent 7b1395b commit 34a1033
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.LineSegment;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.Location;
Expand Down Expand Up @@ -327,57 +328,75 @@ private void addRingSide(Coordinate[] coord, double offsetDistance, int side, in
* <p>
* See https://github.com/locationtech/jts/issues/472
*
* @param inputPts the input ring
* @param inputRing the input ring
* @param distance the buffer distance
* @param curvePts the generated offset curve
* @param curveRing the generated offset curve ring
* @return true if the offset curve is inverted
*/
private static boolean isRingCurveInverted(Coordinate[] inputPts, double distance, Coordinate[] curvePts) {
private static boolean isRingCurveInverted(Coordinate[] inputRing, double distance, Coordinate[] curveRing) {
if (distance == 0.0) return false;
/**
* Only proper rings can invert.
*/
if (inputPts.length <= 3) return false;
if (inputRing.length <= 3) return false;
/**
* Heuristic based on low chance that a ring with many vertices will invert.
* This low limit ensures this test is fairly efficient.
*/
if (inputPts.length >= MAX_INVERTED_RING_SIZE) return false;
if (inputRing.length >= MAX_INVERTED_RING_SIZE) return false;

/**
* Don't check curves which are much larger than the input.
* This improves performance by avoiding checking some concave inputs
* (which can produce fillet arcs with many more vertices)
*/
if (curvePts.length > INVERTED_CURVE_VERTEX_FACTOR * inputPts.length) return false;
if (curveRing.length > INVERTED_CURVE_VERTEX_FACTOR * inputRing.length) return false;

/**
* Check if the curve vertices are all closer to the input ring
* than the buffer distance.
* If so, the curve is NOT a valid buffer curve.
* If curve contains points which are on the buffer,
* it is not inverted and can be included in the raw curves.
*/
double distTol = NEARNESS_FACTOR * Math.abs(distance);
double maxDist = maxDistance(curvePts, inputPts);
boolean isCurveTooClose = maxDist < distTol;
return isCurveTooClose;
if (hasPointOnBuffer(inputRing, distance, curveRing))
return false;

//-- curve is inverted, so discard it
return true;
}

/**
* Computes the maximum distance out of a set of points to a linestring.
* Tests if there are points on the raw offset curve which may
* lie on the final buffer curve
* (i.e. they are (approximately) at the buffer distance from the input ring).
* For efficiency this only tests a limited set of points on the curve.
*
* @param pts the points
* @param line the linestring vertices
* @return the maximum distance
* @param inputRing
* @param distance
* @param curveRing
* @return true if the curve contains points lying at the required buffer distance
*/
private static double maxDistance(Coordinate[] pts, Coordinate[] line) {
double maxDistance = 0;
for (Coordinate p : pts) {
double dist = Distance.pointToSegmentString(p, line);
if (dist > maxDistance) {
maxDistance = dist;
private static boolean hasPointOnBuffer(Coordinate[] inputRing, double distance, Coordinate[] curveRing) {
double distTol = NEARNESS_FACTOR * Math.abs(distance);

for (int i = 0; i < curveRing.length - 1; i++) {
Coordinate v = curveRing[i];

//-- check curve vertices
double dist = Distance.pointToSegmentString(v, inputRing);
if (dist > distTol) {
return true;
}

//-- check curve segment midpoints
int iNext = (i < curveRing.length - 1) ? i + 1 : 0;
Coordinate vnext = curveRing[iNext];
Coordinate midPt = LineSegment.midPoint(v, vnext);

double distMid = Distance.pointToSegmentString(midPt, inputRing);
if (distMid > distTol) {
return true;
}
}
return maxDistance;
return false;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,22 @@ public void testLineClosedNoHole() {
checkBufferHasHole(wkt, 70, false);
}

public void testSmallPolygonNegativeBuffer_1() {
String wkt = "MULTIPOLYGON (((833454.7163917861 6312507.405413097, 833455.3726665961 6312510.208920742, 833456.301153878 6312514.207390314, 833492.2432584754 6312537.770332065, 833493.0901320165 6312536.098774815, 833502.6580673696 6312517.561360772, 833503.9404352929 6312515.0542803425, 833454.7163917861 6312507.405413097)))";
checkBuffer(wkt, -3.8,
"POLYGON ((833459.9671068499 6312512.066918822, 833490.7876785189 6312532.272283619, 833498.1465258132 6312517.999574621, 833459.9671068499 6312512.066918822))");
checkBuffer(wkt, -7,
"POLYGON ((833474.0912127121 6312517.50004999, 833489.5713439264 6312527.648521655, 833493.2674441456 6312520.479822435, 833474.0912127121 6312517.50004999))");
}

public void testSmallPolygonNegativeBuffer_2() {
String wkt = "POLYGON ((182719.04521570954238996 224897.14115349075291306, 182807.02887436276068911 224880.64421749324537814, 182808.47314301913138479 224877.25002362736267969, 182718.38701137207681313 224740.00115247094072402, 182711.82697281913715415 224742.08599378637154587, 182717.1393717635946814 224895.61432328051887453, 182719.04521570954238996 224897.14115349075291306))";
checkBuffer(wkt, -5,
"POLYGON ((182717 224746.99999999997, 182722.00000000003 224891.5, 182801.99999999997 224876.49999999997, 182717 224746.99999999997))");
checkBuffer(wkt, -30,
"POLYGON ((182745.07127364463 224835.32741176756, 182745.97926048582 224861.56823147752, 182760.5070499446 224858.844270954, 182745.07127364463 224835.32741176756))");
}

/**
* See GEOS PR https://github.com/libgeos/geos/pull/978
*/
Expand Down Expand Up @@ -623,6 +639,13 @@ private void checkBuffer(String wkt, double dist, BufferParameters param, String
checkEqual(expected, result, 0.01);
}

private void checkBuffer(String wkt, double dist, String wktExpected) {
Geometry geom = read(wkt);
Geometry result = BufferOp.bufferOp(geom, dist);
Geometry expected = read(wktExpected);
checkEqual(expected, result, 0.01);
}

private void checkBufferEmpty(String wkt, double dist, boolean isEmptyExpected) {
Geometry a = read(wkt);
Geometry result = a.buffer(dist);
Expand Down

0 comments on commit 34a1033

Please sign in to comment.