Skip to content

Commit

Permalink
Fix CCW ring buffer generation
Browse files Browse the repository at this point in the history
  • Loading branch information
dr-jts committed Feb 11, 2025
1 parent 81abe3c commit 6bdb50a
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ private void addLineString(LineString line)
* Singled-sided buffers currently treat rings as if they are lines.
*/
if (CoordinateArrays.isRing(coord) && ! curveBuilder.getBufferParameters().isSingleSided()) {
addRingBothSides(coord, distance);
addLinearRingSides(coord, distance);
}
else {
Coordinate[] curve = curveBuilder.getLineCurve(coord, distance);
Expand Down Expand Up @@ -224,7 +224,7 @@ private void addPolygon(Polygon p)
if (distance <= 0.0 && shellCoord.length < 3)
return;

addRingSide(
addPolygonRingSide(
shellCoord,
offsetDistance,
offsetSide,
Expand All @@ -244,7 +244,7 @@ private void addPolygon(Polygon p)
// Holes are topologically labelled opposite to the shell, since
// the interior of the polygon lies on their opposite side
// (on the left, if the hole is oriented CCW)
addRingSide(
addPolygonRingSide(
holeCoord,
offsetDistance,
Position.opposite(offsetSide),
Expand All @@ -253,46 +253,20 @@ private void addPolygon(Polygon p)
}
}

private void addRingBothSides(Coordinate[] coord, double distance)
{
/*
* (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);
}
}

/**
* Adds an offset curve for one side of a ring.
* Adds an offset curve for one side of a polygon ring.
* The side and left and right topological location arguments
* are provided as if the ring is oriented CW.
* (If the ring is in the opposite orientation,
* this is detected and
* the left and right locations are interchanged and the side is flipped.)
* If the ring is in the opposite orientation,
* the left and right locations are interchanged and the side is flipped.
*
* @param coord the coordinates of the ring (must not contain repeated points)
* @param offsetDistance the positive distance at which to create the buffer
* @param side the side {@link Position} of the ring on which to construct the buffer line
* @param cwLeftLoc the location on the L side of the ring (if it is CW)
* @param cwRightLoc the location on the R side of the ring (if it is CW)
*/
private void addRingSide(Coordinate[] coord, double offsetDistance, int side, int cwLeftLoc, int cwRightLoc)
private void addPolygonRingSide(Coordinate[] coord, double offsetDistance, int side, int cwLeftLoc, int cwRightLoc)
{
// don't bother adding ring if it is "flat" and will disappear in the output
if (offsetDistance == 0.0 && coord.length < LinearRing.MINIMUM_VALID_SIZE)
Expand All @@ -307,16 +281,51 @@ private void addRingSide(Coordinate[] coord, double offsetDistance, int side, in
rightLoc = cwLeftLoc;
side = Position.opposite(side);
}
Coordinate[] curve = curveBuilder.getRingCurve(coord, side, offsetDistance);
addRingSide(coord, offsetDistance, side, leftLoc, rightLoc);
}

/**
* Add both sides of a linear ring.
* Checks for erosion of the hole side.
*
* @param coord ring vertices
* @param distance offset distance (must be non-zero positive)
*/
private void addLinearRingSides(Coordinate[] coord, double distance)
{
/*
* (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 > 0, 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);
}
}

private void addRingSide(Coordinate[] coord, double offsetDistance, int side, int leftLoc, int rightLoc)
{
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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,14 @@ public void testRingHoleEroded() {
"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))");
}

// Checks that a CCW ring generates a correct buffer
// see https://github.com/libgeos/geos/issues/1236
public void testRingCCW() {
String wkt = "LINEARRING (-0.25 0.25, -0.25 0.75, -0.75 0.75, -0.75 0.25, -0.25 0.25)";
checkBuffer(wkt, 1,
"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))");
}

//===================================================

private static BufferParameters bufParamRoundMitre(double mitreLimit) {
Expand Down
51 changes: 51 additions & 0 deletions modules/tests/src/test/resources/testxml/general/TestBuffer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,57 @@
</op></test>
</case>

<case>
<desc>
Closed Line
</desc>
<a>
LINESTRING (1 9, 9 9, 9 1, 1 1, 1 9)
</a>
<test><op name='buffer' arg1='A' arg2='-1.0'> POLYGON EMPTY </op></test>
<test><op name='buffer' arg1='A' arg2='0.0'> POLYGON EMPTY </op></test>
<test><op name='buffer' arg1='A' arg2='1'>
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))
</op></test>
<test><op name='buffer' arg1='A' arg2='10.0'>
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))
</op></test>
</case>

<case>
<desc>
Closed Line - CCW
</desc>
<a>
LINESTRING (1 9, 1 1, 9 1, 9 9, 1 9)
</a>
<test><op name='buffer' arg1='A' arg2='-1.0'> POLYGON EMPTY </op></test>
<test><op name='buffer' arg1='A' arg2='0.0'> POLYGON EMPTY </op></test>
<test><op name='buffer' arg1='A' arg2='1'>
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))
</op></test>
<test><op name='buffer' arg1='A' arg2='10.0'>
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))
</op></test>
</case>

<case>
<desc>
Linear Ring
</desc>
<a>
LINEARRING (1 9, 1 1, 9 1, 9 9, 1 9)
</a>
<test><op name='buffer' arg1='A' arg2='-1.0'> POLYGON EMPTY </op></test>
<test><op name='buffer' arg1='A' arg2='0.0'> POLYGON EMPTY </op></test>
<test><op name='buffer' arg1='A' arg2='1'>
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))
</op></test>
<test><op name='buffer' arg1='A' arg2='10.0'>
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))
</op></test>
</case>

<case>
<desc>
Polygon
Expand Down

0 comments on commit 6bdb50a

Please sign in to comment.