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

Fix buffer to remove inverted ring curves #706

Merged
merged 1 commit into from
Apr 12, 2021
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 @@ -295,10 +295,94 @@ private void addRingSide(Coordinate[] coord, double offsetDistance, int side, in
side = Position.opposite(side);
}
Coordinate[] curve = curveBuilder.getRingCurve(coord, side, offsetDistance);

/**
* If the offset curve has inverted completely it will produce
* an unwanted artifact in the result, so skip it.
*/
if (isRingCurveInverted(coord, offsetDistance, curve)) {
return;
}

addCurve(curve, leftLoc, rightLoc);
}

private static final int MAX_INVERTED_RING_SIZE = 9;
private static final double NEARNESS_FACTOR = 0.99;

/**
* Tests whether the offset curve for a ring is fully inverted.
* An inverted ("inside-out") curve occurs in some specific situations
* involving a buffer distance which should result in a fully-eroded (empty) buffer.
* It can happen that the sides of a small, convex polygon
* produce offset segments which all cross one another to form
* a curve with inverted orientation.
* This happens at buffer distances slightly greater than the distance at
* which the buffer should disappear.
* The inverted curve will produce an incorrect non-empty buffer (for a shell)
* or an incorrect hole (for a hole).
* It must be discarded from the set of offset curves used in the buffer.
* Heuristics are used to reduce the number of cases which area checked,
* for efficiency and correctness.
* <p>
* See https://github.com/locationtech/jts/issues/472
*
* @param inputPts the input ring
* @param distance the buffer distance
* @param curvePts the generated offset curve
* @return true if the offset curve is inverted
*/
private static boolean isRingCurveInverted(Coordinate[] inputPts, double distance, Coordinate[] curvePts) {
if (distance == 0.0) return false;
/**
* Only proper rings can invert.
*/
if (inputPts.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;

/**
* An inverted curve has no more points than the input ring.
* This also eliminates concave inputs (which will produce fillet arcs)
*/
if (curvePts.length > inputPts.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.
*/
double distTol = NEARNESS_FACTOR * Math.abs(distance);
double maxDist = maxDistance(curvePts, inputPts);
boolean isCurveTooClose = maxDist < distTol;
return isCurveTooClose;
}

/**
* Computes the maximum distance out of a set of points to a linestring.
*
* @param pts the points
* @param line the linestring vertices
* @return the maximum 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;
}
}
return maxDistance;
}

/**
* Tests whether a ring buffer is eroded completely (is empty)
* based on simple heuristics.
*
* The ringCoord is assumed to contain no repeated points.
* It may be degenerate (i.e. contain only 1, 2, or 3 points).
* In this case it has no area, and hence has a minimum diameter of 0.
Expand All @@ -307,7 +391,7 @@ private void addRingSide(Coordinate[] coord, double offsetDistance, int side, in
* @param offsetDistance
* @return
*/
private boolean isErodedCompletely(LinearRing ring, double bufferDistance)
private static boolean isErodedCompletely(LinearRing ring, double bufferDistance)
{
Coordinate[] ringCoord = ring.getCoordinates();
// degenerate ring has no area
Expand All @@ -327,24 +411,6 @@ private boolean isErodedCompletely(LinearRing ring, double bufferDistance)
return true;

return false;
/**
* The following is a heuristic test to determine whether an
* inside buffer will be eroded completely.
* It is based on the fact that the minimum diameter of the ring pointset
* provides an upper bound on the buffer distance which would erode the
* ring.
* If the buffer distance is less than the minimum diameter, the ring
* may still be eroded, but this will be determined by
* a full topological computation.
*
*/
//System.out.println(ring);
/* MD 7 Feb 2005 - there's an unknown bug in the MD code, so disable this for now
MinimumDiameter md = new MinimumDiameter(ring);
minDiam = md.getLength();
//System.out.println(md.getDiameter());
return minDiam < 2 * Math.abs(bufferDistance);
*/
}

/**
Expand All @@ -364,7 +430,7 @@ private boolean isErodedCompletely(LinearRing ring, double bufferDistance)
* @param bufferDistance
* @return
*/
private boolean isTriangleErodedCompletely(
private static boolean isTriangleErodedCompletely(
Coordinate[] triangleCoord,
double bufferDistance)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.PrecisionModel;

import test.jts.GeometryTestCase;
Expand Down Expand Up @@ -524,4 +525,71 @@ public void testBowtiePolygonHoleLargestAreaRetained() {
Geometry expected = read("POLYGON ((0 40, 60 40, 60 0, 0 0, 0 40), (10 10, 50 10, 30 30, 10 10))");
checkEqual(expected, result);
}

/**
* Following tests check "inverted ring" issue.
* https://github.com/locationtech/jts/issues/472
*/

public void testPolygon4NegBufferEmpty() {
String wkt = "POLYGON ((666360.09 429614.71, 666344.4 429597.12, 666358.47 429584.52, 666374.5 429602.33, 666360.09 429614.71))";
checkBufferEmpty(wkt, -9, false);
checkBufferEmpty(wkt, -10, true);
checkBufferEmpty(wkt, -15, true);
checkBufferEmpty(wkt, -18, true);
}

public void testPolygon5NegBufferEmpty() {
String wkt = "POLYGON ((6 20, 16 20, 21 9, 9 0, 0 10, 6 20))";
checkBufferEmpty(wkt, -8, false);
checkBufferEmpty(wkt, -8.6, true);
checkBufferEmpty(wkt, -9.6, true);
checkBufferEmpty(wkt, -11, true);
}

public void testPolygonHole5BufferNoHole() {
String wkt = "POLYGON ((-6 26, 29 26, 29 -5, -6 -5, -6 26), (6 20, 16 20, 21 9, 9 0, 0 10, 6 20))";
checkBufferHasHole(wkt, 8, true);
checkBufferHasHole(wkt, 8.6, false);
checkBufferHasHole(wkt, 9.6, false);
checkBufferHasHole(wkt, 11, false);
}

/**
* Tests that an inverted ring curve in an element of a MultiPolygon is removed
*/
public void testMultiPolygonElementRemoved() {
String wkt = "MULTIPOLYGON (((30 18, 14 0, 0 13, 16 30, 30 18)), ((180 210, 60 50, 154 6, 270 40, 290 130, 250 190, 180 210)))";
checkBufferNumGeometries(wkt, -9, 2);
checkBufferNumGeometries(wkt, -10, 1);
checkBufferNumGeometries(wkt, -15, 1);
checkBufferNumGeometries(wkt, -18, 1);
}

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

private void checkBufferHasHole(String wkt, double dist, boolean isHoleExpected) {
Geometry a = read(wkt);
Geometry result = a.buffer(dist);
assertTrue(isHoleExpected == hasHole(result));
}

private void checkBufferNumGeometries(String wkt, double dist, int numExpected) {
Geometry a = read(wkt);
Geometry result = a.buffer(dist);
assertTrue(numExpected == result.getNumGeometries());
}

private boolean hasHole(Geometry geom) {
for (int i = 0; i < geom.getNumGeometries(); i++) {
Polygon poly = (Polygon) geom.getGeometryN(i);
if (poly.getNumInteriorRing() > 0) return true;
}
return false;
}

}