Skip to content

Commit 6bdb50a

Browse files
committed
Fix CCW ring buffer generation
1 parent 81abe3c commit 6bdb50a

File tree

3 files changed

+103
-35
lines changed

3 files changed

+103
-35
lines changed

modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferCurveSetBuilder.java

Lines changed: 44 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ private void addLineString(LineString line)
184184
* Singled-sided buffers currently treat rings as if they are lines.
185185
*/
186186
if (CoordinateArrays.isRing(coord) && ! curveBuilder.getBufferParameters().isSingleSided()) {
187-
addRingBothSides(coord, distance);
187+
addLinearRingSides(coord, distance);
188188
}
189189
else {
190190
Coordinate[] curve = curveBuilder.getLineCurve(coord, distance);
@@ -224,7 +224,7 @@ private void addPolygon(Polygon p)
224224
if (distance <= 0.0 && shellCoord.length < 3)
225225
return;
226226

227-
addRingSide(
227+
addPolygonRingSide(
228228
shellCoord,
229229
offsetDistance,
230230
offsetSide,
@@ -244,7 +244,7 @@ private void addPolygon(Polygon p)
244244
// Holes are topologically labelled opposite to the shell, since
245245
// the interior of the polygon lies on their opposite side
246246
// (on the left, if the hole is oriented CCW)
247-
addRingSide(
247+
addPolygonRingSide(
248248
holeCoord,
249249
offsetDistance,
250250
Position.opposite(offsetSide),
@@ -253,46 +253,20 @@ private void addPolygon(Polygon p)
253253
}
254254
}
255255

256-
private void addRingBothSides(Coordinate[] coord, double distance)
257-
{
258-
/*
259-
* (f "hole" side will be eroded completely, avoid generating it.
260-
* This prevents hole artifacts (e.g. https://github.com/libgeos/geos/issues/1223)
261-
*/
262-
//-- distance is assumed positive, due to previous checks
263-
boolean isHoleComputed = ! isRingFullyEroded(coord, CoordinateArrays.envelope(coord), true, distance);
264-
265-
boolean isCCW = isRingCCW(coord);
266-
267-
boolean isShellLeft = ! isCCW;
268-
if (isShellLeft || isHoleComputed) {
269-
addRingSide(coord, distance,
270-
Position.LEFT,
271-
Location.EXTERIOR, Location.INTERIOR);
272-
}
273-
boolean isShellRight = isCCW;
274-
if (isShellRight || isHoleComputed) {
275-
addRingSide(coord, distance,
276-
Position.RIGHT,
277-
Location.INTERIOR, Location.EXTERIOR);
278-
}
279-
}
280-
281256
/**
282-
* Adds an offset curve for one side of a ring.
257+
* Adds an offset curve for one side of a polygon ring.
283258
* The side and left and right topological location arguments
284259
* are provided as if the ring is oriented CW.
285-
* (If the ring is in the opposite orientation,
286-
* this is detected and
287-
* the left and right locations are interchanged and the side is flipped.)
260+
* If the ring is in the opposite orientation,
261+
* the left and right locations are interchanged and the side is flipped.
288262
*
289263
* @param coord the coordinates of the ring (must not contain repeated points)
290264
* @param offsetDistance the positive distance at which to create the buffer
291265
* @param side the side {@link Position} of the ring on which to construct the buffer line
292266
* @param cwLeftLoc the location on the L side of the ring (if it is CW)
293267
* @param cwRightLoc the location on the R side of the ring (if it is CW)
294268
*/
295-
private void addRingSide(Coordinate[] coord, double offsetDistance, int side, int cwLeftLoc, int cwRightLoc)
269+
private void addPolygonRingSide(Coordinate[] coord, double offsetDistance, int side, int cwLeftLoc, int cwRightLoc)
296270
{
297271
// don't bother adding ring if it is "flat" and will disappear in the output
298272
if (offsetDistance == 0.0 && coord.length < LinearRing.MINIMUM_VALID_SIZE)
@@ -307,16 +281,51 @@ private void addRingSide(Coordinate[] coord, double offsetDistance, int side, in
307281
rightLoc = cwLeftLoc;
308282
side = Position.opposite(side);
309283
}
310-
Coordinate[] curve = curveBuilder.getRingCurve(coord, side, offsetDistance);
284+
addRingSide(coord, offsetDistance, side, leftLoc, rightLoc);
285+
}
286+
287+
/**
288+
* Add both sides of a linear ring.
289+
* Checks for erosion of the hole side.
290+
*
291+
* @param coord ring vertices
292+
* @param distance offset distance (must be non-zero positive)
293+
*/
294+
private void addLinearRingSides(Coordinate[] coord, double distance)
295+
{
296+
/*
297+
* (f "hole" side will be eroded completely, avoid generating it.
298+
* This prevents hole artifacts (e.g. https://github.com/libgeos/geos/issues/1223)
299+
*/
300+
//-- distance is assumed > 0, due to previous checks
301+
boolean isHoleComputed = ! isRingFullyEroded(coord, CoordinateArrays.envelope(coord), true, distance);
302+
303+
boolean isCCW = isRingCCW(coord);
311304

305+
boolean isShellLeft = ! isCCW;
306+
if (isShellLeft || isHoleComputed) {
307+
addRingSide(coord, distance,
308+
Position.LEFT,
309+
Location.EXTERIOR, Location.INTERIOR);
310+
}
311+
boolean isShellRight = isCCW;
312+
if (isShellRight || isHoleComputed) {
313+
addRingSide(coord, distance,
314+
Position.RIGHT,
315+
Location.INTERIOR, Location.EXTERIOR);
316+
}
317+
}
318+
319+
private void addRingSide(Coordinate[] coord, double offsetDistance, int side, int leftLoc, int rightLoc)
320+
{
321+
Coordinate[] curve = curveBuilder.getRingCurve(coord, side, offsetDistance);
312322
/**
313323
* If the offset curve has inverted completely it will produce
314324
* an unwanted artifact in the result, so skip it.
315325
*/
316326
if (isRingCurveInverted(coord, offsetDistance, curve)) {
317327
return;
318328
}
319-
320329
addCurve(curve, leftLoc, rightLoc);
321330
}
322331

modules/core/src/test/java/org/locationtech/jts/operation/buffer/BufferTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,14 @@ public void testRingHoleEroded() {
642642
"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))");
643643
}
644644

645+
// Checks that a CCW ring generates a correct buffer
646+
// see https://github.com/libgeos/geos/issues/1236
647+
public void testRingCCW() {
648+
String wkt = "LINEARRING (-0.25 0.25, -0.25 0.75, -0.75 0.75, -0.75 0.25, -0.25 0.25)";
649+
checkBuffer(wkt, 1,
650+
"POLYGON ((0.73 0.05, 0.67 -0.13, 0.58 -0.31, 0.46 -0.46, 0.31 -0.58, 0.13 -0.67, -0.05 -0.73, -0.25 -0.75, -0.75 -0.75, -0.95 -0.73, -1.13 -0.67, -1.31 -0.58, -1.46 -0.46, -1.58 -0.31, -1.67 -0.13, -1.73 0.05, -1.75 0.25, -1.75 0.75, -1.73 0.95, -1.67 1.13, -1.58 1.31, -1.46 1.46, -1.31 1.58, -1.13 1.67, -0.95 1.73, -0.75 1.75, -0.25 1.75, -0.05 1.73, 0.13 1.67, 0.31 1.58, 0.46 1.46, 0.58 1.31, 0.67 1.13, 0.73 0.95, 0.75 0.75, 0.75 0.25, 0.73 0.05))");
651+
}
652+
645653
//===================================================
646654

647655
private static BufferParameters bufParamRoundMitre(double mitreLimit) {

modules/tests/src/test/resources/testxml/general/TestBuffer.xml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,57 @@
3333
</op></test>
3434
</case>
3535

36+
<case>
37+
<desc>
38+
Closed Line
39+
</desc>
40+
<a>
41+
LINESTRING (1 9, 9 9, 9 1, 1 1, 1 9)
42+
</a>
43+
<test><op name='buffer' arg1='A' arg2='-1.0'> POLYGON EMPTY </op></test>
44+
<test><op name='buffer' arg1='A' arg2='0.0'> POLYGON EMPTY </op></test>
45+
<test><op name='buffer' arg1='A' arg2='1'>
46+
POLYGON ((1 10, 9 10, 9.195090322016128 9.98078528040323, 9.38268343236509 9.923879532511286, 9.555570233019603 9.831469612302545, 9.707106781186548 9.707106781186548, 9.831469612302545 9.555570233019601, 9.923879532511286 9.38268343236509, 9.98078528040323 9.195090322016128, 10 9, 10 1, 9.98078528040323 0.8049096779838718, 9.923879532511286 0.6173165676349102, 9.831469612302545 0.4444297669803978, 9.707106781186548 0.2928932188134525, 9.555570233019603 0.1685303876974548, 9.38268343236509 0.0761204674887133, 9.195090322016128 0.0192147195967696, 9 0, 1 0, 0.8049096779838718 0.0192147195967696, 0.6173165676349103 0.0761204674887133, 0.444429766980398 0.1685303876974547, 0.2928932188134525 0.2928932188134524, 0.1685303876974547 0.4444297669803978, 0.0761204674887133 0.6173165676349102, 0.0192147195967696 0.8049096779838714, 0 1, 0 9, 0.0192147195967696 9.195090322016128, 0.0761204674887133 9.38268343236509, 0.1685303876974547 9.555570233019601, 0.2928932188134525 9.707106781186548, 0.444429766980398 9.831469612302545, 0.6173165676349103 9.923879532511286, 0.8049096779838718 9.98078528040323, 1 10), (2 8, 2 2, 8 2, 8 8, 2 8))
47+
</op></test>
48+
<test><op name='buffer' arg1='A' arg2='10.0'>
49+
POLYGON ((1 19, 9 19, 10.950903220161283 18.807852804032304, 12.826834323650898 18.238795325112868, 14.555702330196024 17.314696123025453, 16.071067811865476 16.071067811865476, 17.314696123025453 14.555702330196022, 18.238795325112868 12.826834323650898, 18.807852804032304 10.950903220161283, 19 9, 19 1, 18.807852804032304 -0.9509032201612824, 18.238795325112868 -2.826834323650898, 17.314696123025453 -4.555702330196022, 16.071067811865476 -6.071067811865475, 14.555702330196024 -7.314696123025453, 12.826834323650898 -8.238795325112868, 10.950903220161283 -8.807852804032304, 9 -9, 1 -9, -0.9509032201612819 -8.807852804032304, -2.826834323650897 -8.238795325112868, -4.55570233019602 -7.314696123025453, -6.071067811865475 -6.0710678118654755, -7.314696123025453 -4.555702330196022, -8.238795325112868 -2.826834323650899, -8.807852804032304 -0.9509032201612861, -9 1, -9 9, -8.807852804032304 10.950903220161287, -8.238795325112868 12.8268343236509, -7.314696123025453 14.555702330196022, -6.071067811865475 16.071067811865476, -4.55570233019602 17.314696123025453, -2.826834323650897 18.238795325112868, -0.9509032201612819 18.807852804032304, 1 19))
50+
</op></test>
51+
</case>
52+
53+
<case>
54+
<desc>
55+
Closed Line - CCW
56+
</desc>
57+
<a>
58+
LINESTRING (1 9, 1 1, 9 1, 9 9, 1 9)
59+
</a>
60+
<test><op name='buffer' arg1='A' arg2='-1.0'> POLYGON EMPTY </op></test>
61+
<test><op name='buffer' arg1='A' arg2='0.0'> POLYGON EMPTY </op></test>
62+
<test><op name='buffer' arg1='A' arg2='1'>
63+
POLYGON ((1 10, 9 10, 9.195090322016128 9.98078528040323, 9.38268343236509 9.923879532511286, 9.555570233019603 9.831469612302545, 9.707106781186548 9.707106781186548, 9.831469612302545 9.555570233019601, 9.923879532511286 9.38268343236509, 9.98078528040323 9.195090322016128, 10 9, 10 1, 9.98078528040323 0.8049096779838718, 9.923879532511286 0.6173165676349102, 9.831469612302545 0.4444297669803978, 9.707106781186548 0.2928932188134525, 9.555570233019603 0.1685303876974548, 9.38268343236509 0.0761204674887133, 9.195090322016128 0.0192147195967696, 9 0, 1 0, 0.8049096779838718 0.0192147195967696, 0.6173165676349103 0.0761204674887133, 0.444429766980398 0.1685303876974547, 0.2928932188134525 0.2928932188134524, 0.1685303876974547 0.4444297669803978, 0.0761204674887133 0.6173165676349102, 0.0192147195967696 0.8049096779838714, 0 1, 0 9, 0.0192147195967696 9.195090322016128, 0.0761204674887133 9.38268343236509, 0.1685303876974547 9.555570233019601, 0.2928932188134525 9.707106781186548, 0.444429766980398 9.831469612302545, 0.6173165676349103 9.923879532511286, 0.8049096779838718 9.98078528040323, 1 10), (2 8, 2 2, 8 2, 8 8, 2 8))
64+
</op></test>
65+
<test><op name='buffer' arg1='A' arg2='10.0'>
66+
POLYGON ((1 19, 9 19, 10.950903220161283 18.807852804032304, 12.826834323650898 18.238795325112868, 14.555702330196024 17.314696123025453, 16.071067811865476 16.071067811865476, 17.314696123025453 14.555702330196022, 18.238795325112868 12.826834323650898, 18.807852804032304 10.950903220161283, 19 9, 19 1, 18.807852804032304 -0.9509032201612824, 18.238795325112868 -2.826834323650898, 17.314696123025453 -4.555702330196022, 16.071067811865476 -6.071067811865475, 14.555702330196024 -7.314696123025453, 12.826834323650898 -8.238795325112868, 10.950903220161283 -8.807852804032304, 9 -9, 1 -9, -0.9509032201612819 -8.807852804032304, -2.826834323650897 -8.238795325112868, -4.55570233019602 -7.314696123025453, -6.071067811865475 -6.0710678118654755, -7.314696123025453 -4.555702330196022, -8.238795325112868 -2.826834323650899, -8.807852804032304 -0.9509032201612861, -9 1, -9 9, -8.807852804032304 10.950903220161287, -8.238795325112868 12.8268343236509, -7.314696123025453 14.555702330196022, -6.071067811865475 16.071067811865476, -4.55570233019602 17.314696123025453, -2.826834323650897 18.238795325112868, -0.9509032201612819 18.807852804032304, 1 19))
67+
</op></test>
68+
</case>
69+
70+
<case>
71+
<desc>
72+
Linear Ring
73+
</desc>
74+
<a>
75+
LINEARRING (1 9, 1 1, 9 1, 9 9, 1 9)
76+
</a>
77+
<test><op name='buffer' arg1='A' arg2='-1.0'> POLYGON EMPTY </op></test>
78+
<test><op name='buffer' arg1='A' arg2='0.0'> POLYGON EMPTY </op></test>
79+
<test><op name='buffer' arg1='A' arg2='1'>
80+
POLYGON ((1 10, 9 10, 9.195090322016128 9.98078528040323, 9.38268343236509 9.923879532511286, 9.555570233019603 9.831469612302545, 9.707106781186548 9.707106781186548, 9.831469612302545 9.555570233019601, 9.923879532511286 9.38268343236509, 9.98078528040323 9.195090322016128, 10 9, 10 1, 9.98078528040323 0.8049096779838718, 9.923879532511286 0.6173165676349102, 9.831469612302545 0.4444297669803978, 9.707106781186548 0.2928932188134525, 9.555570233019603 0.1685303876974548, 9.38268343236509 0.0761204674887133, 9.195090322016128 0.0192147195967696, 9 0, 1 0, 0.8049096779838718 0.0192147195967696, 0.6173165676349103 0.0761204674887133, 0.444429766980398 0.1685303876974547, 0.2928932188134525 0.2928932188134524, 0.1685303876974547 0.4444297669803978, 0.0761204674887133 0.6173165676349102, 0.0192147195967696 0.8049096779838714, 0 1, 0 9, 0.0192147195967696 9.195090322016128, 0.0761204674887133 9.38268343236509, 0.1685303876974547 9.555570233019601, 0.2928932188134525 9.707106781186548, 0.444429766980398 9.831469612302545, 0.6173165676349103 9.923879532511286, 0.8049096779838718 9.98078528040323, 1 10), (2 8, 2 2, 8 2, 8 8, 2 8))
81+
</op></test>
82+
<test><op name='buffer' arg1='A' arg2='10.0'>
83+
POLYGON ((1 19, 9 19, 10.950903220161283 18.807852804032304, 12.826834323650898 18.238795325112868, 14.555702330196024 17.314696123025453, 16.071067811865476 16.071067811865476, 17.314696123025453 14.555702330196022, 18.238795325112868 12.826834323650898, 18.807852804032304 10.950903220161283, 19 9, 19 1, 18.807852804032304 -0.9509032201612824, 18.238795325112868 -2.826834323650898, 17.314696123025453 -4.555702330196022, 16.071067811865476 -6.071067811865475, 14.555702330196024 -7.314696123025453, 12.826834323650898 -8.238795325112868, 10.950903220161283 -8.807852804032304, 9 -9, 1 -9, -0.9509032201612819 -8.807852804032304, -2.826834323650897 -8.238795325112868, -4.55570233019602 -7.314696123025453, -6.071067811865475 -6.0710678118654755, -7.314696123025453 -4.555702330196022, -8.238795325112868 -2.826834323650899, -8.807852804032304 -0.9509032201612861, -9 1, -9 9, -8.807852804032304 10.950903220161287, -8.238795325112868 12.8268343236509, -7.314696123025453 14.555702330196022, -6.071067811865475 16.071067811865476, -4.55570233019602 17.314696123025453, -2.826834323650897 18.238795325112868, -0.9509032201612819 18.807852804032304, 1 19))
84+
</op></test>
85+
</case>
86+
3687
<case>
3788
<desc>
3889
Polygon

0 commit comments

Comments
 (0)