From 9f0f525e30788d08fcdc29837c8948a96f16cee6 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 5 Feb 2025 10:09:05 -0800 Subject: [PATCH] Fix ring buffer hole erosion --- .../buffer/BufferCurveSetBuilder.java | 64 ++++++++++++------- .../jts/operation/buffer/BufferTest.java | 11 ++++ 2 files changed, 53 insertions(+), 22 deletions(-) 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 11f68f17db..77052cba77 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 fbe1b2585b..034eaa7d56 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) {