diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferCurveSetBuilder.java b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferCurveSetBuilder.java index 11f68f17d..77052cba7 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferCurveSetBuilder.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferCurveSetBuilder.java @@ -216,9 +216,9 @@ private void addPolygon(Polygon p) LinearRing shell = p.getExteriorRing(); Coordinate[] shellCoord = clean(shell.getCoordinates()); - // optimization - don't bother computing buffer + // optimization - don't compute buffer // if the polygon would be completely eroded - if (distance < 0.0 && isErodedCompletely(shell, distance)) + if (distance < 0.0 && isRingFullyEroded(shell, false, distance)) return; // don't attempt to buffer a polygon with too few distinct vertices if (distance <= 0.0 && shellCoord.length < 3) @@ -236,9 +236,9 @@ private void addPolygon(Polygon p) LinearRing hole = p.getInteriorRingN(i); Coordinate[] holeCoord = clean(hole.getCoordinates()); - // optimization - don't bother computing buffer for this hole + // optimization - don't compute buffer for this hole // if the hole would be completely covered - if (distance > 0.0 && isErodedCompletely(hole, -distance)) + if (distance > 0.0 && isRingFullyEroded(hole, true, distance)) continue; // Holes are topologically labelled opposite to the shell, since @@ -255,14 +255,27 @@ private void addPolygon(Polygon p) private void addRingBothSides(Coordinate[] coord, double distance) { - addRingSide(coord, distance, - Position.LEFT, - Location.EXTERIOR, Location.INTERIOR); - /* Add the opposite side of the ring - */ - addRingSide(coord, distance, - Position.RIGHT, - Location.INTERIOR, Location.EXTERIOR); + /* + * (f "hole" side will be eroded completely, avoid generating it. + * This prevents hole artifacts (e.g. https://github.com/libgeos/geos/issues/1223) + */ + //-- distance is assumed positive, due to previous checks + boolean isHoleComputed = ! isRingFullyEroded(coord, CoordinateArrays.envelope(coord), true, distance); + + boolean isCCW = isRingCCW(coord); + + boolean isShellLeft = ! isCCW; + if (isShellLeft || isHoleComputed) { + addRingSide(coord, distance, + Position.LEFT, + Location.EXTERIOR, Location.INTERIOR); + } + boolean isShellRight = isCCW; + if (isShellRight || isHoleComputed) { + addRingSide(coord, distance, + Position.RIGHT, + Location.INTERIOR, Location.EXTERIOR); + } } /** @@ -411,25 +424,32 @@ private static boolean hasPointOnBuffer(Coordinate[] inputRing, double distance, * @param offsetDistance * @return */ - private static boolean isErodedCompletely(LinearRing ring, double bufferDistance) + private static boolean isRingFullyEroded(LinearRing ring, boolean isHole, double bufferDistance) + { + return isRingFullyEroded(ring.getCoordinates(), ring.getEnvelopeInternal(), isHole, bufferDistance); + } + + private static boolean isRingFullyEroded(Coordinate[] ringCoord, Envelope ringEnv, boolean isHole, double bufferDistance) { - Coordinate[] ringCoord = ring.getCoordinates(); // degenerate ring has no area if (ringCoord.length < 4) - return bufferDistance < 0; + return true; // important test to eliminate inverted triangle bug // also optimizes erosion test for triangles if (ringCoord.length == 4) return isTriangleErodedCompletely(ringCoord, bufferDistance); - // if envelope is narrower than twice the buffer distance, ring is eroded - Envelope env = ring.getEnvelopeInternal(); - double envMinDimension = Math.min(env.getHeight(), env.getWidth()); - if (bufferDistance < 0.0 - && 2 * Math.abs(bufferDistance) > envMinDimension) - return true; - + boolean isErodable = + ( isHole && bufferDistance > 0) || + (! isHole && bufferDistance < 0); + + if (isErodable) { + //-- if envelope is narrower than twice the buffer distance, ring is eroded + double envMinDimension = Math.min(ringEnv.getHeight(), ringEnv.getWidth()); + if (2 * Math.abs(bufferDistance) > envMinDimension) + return true; + } return false; } diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/BufferTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/BufferTest.java index fbe1b2585..034eaa7d5 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/BufferTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/BufferTest.java @@ -631,6 +631,17 @@ public void testPolygonQS_1KeepHoles() { assertEquals(geom.getNumInteriorRing(), buf.getNumInteriorRing()); } + //See https://github.com/libgeos/geos/issues/1223 + public void testRingHoleEroded() { + String wkt = "LINESTRING (25 44, 31 44, 32 38, 29 37, 25 37, 25 38, 24 40, 24 44, 25 44)"; + checkBuffer(wkt, 100, +"POLYGON ((50.95 141.99, 70.09 136.04, 87.66 126.4, 102.96 113.44, 115.36 97.69, 124.38 79.78, 129.64 60.44, 130.64 54.44, 131.93 34.31, 129.16 14.34, 122.44 -4.68, 112.03 -21.96, 98.37 -36.8, 82.02 -48.59, 63.62 -56.87, 60.62 -57.87, 45.02 -61.71, 29 -63, 25 -63, 4.33 -60.84, -15.44 -54.46, -33.47 -44.12, -48.97 -30.29, -61.28 -13.55, -69.87 5.38, -70.87 8.38, -74.71 23.98, -76 40, -76 44, -74.08 63.51, -68.39 82.27, -59.15 99.56, -46.71 114.71, -31.56 127.15, -14.27 136.39, 4.49 142.08, 24 144, 31 144, 50.95 141.99))"); + checkBuffer(wkt, 10, + "POLYGON ((15.06 35.53, 14.27 37.7, 14 40, 14 44, 14.19 45.95, 14.76 47.83, 15.69 49.56, 16.93 51.07, 18.44 52.31, 20.17 53.24, 22.05 53.81, 24 54, 31 54, 32.99 53.8, 34.91 53.2, 36.67 52.24, 38.2 50.94, 39.44 49.37, 40.34 47.58, 40.86 45.64, 41.86 39.64, 41.99 37.63, 41.72 35.63, 41.04 33.73, 40 32, 38.64 30.52, 37 29.34, 35.16 28.51, 32.16 27.51, 30.6 27.13, 29 27, 25 27, 23.05 27.19, 21.17 27.76, 19.44 28.69, 17.93 29.93, 16.69 31.44, 15.76 33.17, 15.19 35.05, 15.17 35.31, 15.06 35.53))"); + checkBuffer(wkt, 2, + "POLYGON ((31.4 45.96, 31.78 45.84, 32.13 45.65, 32.44 45.39, 32.69 45.07, 32.87 44.72, 32.97 44.33, 33.97 38.33, 34 37.93, 33.94 37.53, 33.81 37.15, 33.6 36.8, 33.33 36.5, 33 36.27, 32.63 36.1, 29.63 35.1, 29.32 35.03, 29 35, 25 35, 24.61 35.04, 24.23 35.15, 23.89 35.34, 23.59 35.59, 23.34 35.89, 23.15 36.23, 23.04 36.61, 23 37, 23 37.53, 22.21 39.11, 22.05 39.54, 22 40, 22 44, 22.04 44.39, 22.15 44.77, 22.34 45.11, 22.59 45.41, 22.89 45.66, 23.23 45.85, 23.61 45.96, 24 46, 31 46, 31.4 45.96), (26 40.47, 26.74 39, 28.68 39, 29.75 39.36, 29.31 42, 26 42, 26 40.47))"); + } + //=================================================== private static BufferParameters bufParamRoundMitre(double mitreLimit) {