Skip to content

Commit

Permalink
Improve Buffer ring simplfication (#1022)
Browse files Browse the repository at this point in the history
  • Loading branch information
dr-jts authored Nov 21, 2023
1 parent e25f8d9 commit 89bfbab
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,20 @@
import org.locationtech.jts.algorithm.Distance;
import org.locationtech.jts.algorithm.Orientation;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateArrays;
import org.locationtech.jts.geom.CoordinateList;

/**
* Simplifies a buffer input line to
* remove concavities with shallow depth.
* <p>
* The most important benefit of doing this
* The major benefit of doing this
* is to reduce the number of points and the complexity of
* shape which will be buffered.
* This improve performance and robustness.
* It also reduces the risk of gores created by
* the quantized fillet arcs (although this issue
* should be eliminated in any case by the
* should be eliminated by the
* offset curve generation logic).
* <p>
* A key aspect of the simplification is that it
Expand All @@ -35,8 +37,9 @@
* lies at the correct distance from the input geometry.
* <p>
* Another important heuristic used is that the end segments
* of the input are never simplified. This ensures that
* of linear inputs are never simplified. This ensures that
* the client buffer code is able to generate end caps faithfully.
* Ring inputs can have end segments removed by simplification.
* <p>
* No attempt is made to avoid self-intersections in the output.
* This is acceptable for use for generating a buffer offset curve,
Expand Down Expand Up @@ -67,18 +70,18 @@ public static Coordinate[] simplify(Coordinate[] inputLine, double distanceTol)
return simp.simplify(distanceTol);
}

private static final int INIT = 0;
private static final int DELETE = 1;
private static final int KEEP = 1;


private Coordinate[] inputLine;
private double distanceTol;
private byte[] isDeleted;
private boolean isRing;
private boolean[] isDeleted;
private int angleOrientation = Orientation.COUNTERCLOCKWISE;


public BufferInputLineSimplifier(Coordinate[] inputLine) {
this.inputLine = inputLine;
isRing = CoordinateArrays.isRing(inputLine);
}

/**
Expand All @@ -94,11 +97,12 @@ public BufferInputLineSimplifier(Coordinate[] inputLine) {
public Coordinate[] simplify(double distanceTol)
{
this.distanceTol = Math.abs(distanceTol);
angleOrientation = Orientation.COUNTERCLOCKWISE;
if (distanceTol < 0)
angleOrientation = Orientation.CLOCKWISE;

// rely on fact that boolean array is filled with false value
isDeleted = new byte[inputLine.length];
// rely on fact that boolean array is filled with false values
isDeleted = new boolean[inputLine.length];

boolean isChanged = false;
do {
Expand All @@ -112,26 +116,27 @@ public Coordinate[] simplify(double distanceTol)
* Uses a sliding window containing 3 vertices to detect shallow angles
* in which the middle vertex can be deleted, since it does not
* affect the shape of the resulting buffer in a significant way.
* @return
*
* @return true if any vertices were deleted
*/
private boolean deleteShallowConcavities()
{
/**
* Do not simplify end line segments of the line string.
* Do not simplify end line segments of lines.
* This ensures that end caps are generated consistently.
*/
int index = 1;
int index = isRing ? 0 : 1;

int midIndex = findNextNonDeletedIndex(index);
int lastIndex = findNextNonDeletedIndex(midIndex);
int midIndex = nextIndex(index);
int lastIndex = nextIndex(midIndex);

boolean isChanged = false;
while (lastIndex < inputLine.length) {
// test triple for shallow concavity
boolean isMiddleVertexDeleted = false;
if (isDeletable(index, midIndex, lastIndex,
distanceTol)) {
isDeleted[midIndex] = DELETE;
isDeleted[midIndex] = true;
isMiddleVertexDeleted = true;
isChanged = true;
}
Expand All @@ -141,8 +146,8 @@ private boolean deleteShallowConcavities()
else
index = midIndex;

midIndex = findNextNonDeletedIndex(index);
lastIndex = findNextNonDeletedIndex(midIndex);
midIndex = nextIndex(index);
lastIndex = nextIndex(midIndex);
}
return isChanged;
}
Expand All @@ -153,10 +158,10 @@ private boolean deleteShallowConcavities()
* @return the next non-deleted index, if any
* or inputLine.length if there are no more non-deleted indices
*/
private int findNextNonDeletedIndex(int index)
private int nextIndex(int index)
{
int next = index + 1;
while (next < inputLine.length && isDeleted[next] == DELETE)
while (next < inputLine.length && isDeleted[next])
next++;
return next;
}
Expand All @@ -165,10 +170,9 @@ private Coordinate[] collapseLine()
{
CoordinateList coordList = new CoordinateList();
for (int i = 0; i < inputLine.length; i++) {
if (isDeleted[i] != DELETE)
if (! isDeleted[i])
coordList.add(inputLine[i]);
}
// if (coordList.size() < inputLine.length) System.out.println("Simplified " + (inputLine.length - coordList.size()) + " pts");
return coordList.toCoordinateArray();
}

Expand All @@ -181,22 +185,8 @@ private boolean isDeletable(int i0, int i1, int i2, double distanceTol)
if (! isConcave(p0, p1, p2)) return false;
if (! isShallow(p0, p1, p2, distanceTol)) return false;

// MD - don't use this heuristic - it's too restricting
// if (p0.distance(p2) > distanceTol) return false;

return isShallowSampled(p0, p1, i0, i2, distanceTol);
}

private boolean isShallowConcavity(Coordinate p0, Coordinate p1, Coordinate p2, double distanceTol)
{
int orientation = Orientation.index(p0, p1, p2);
boolean isAngleToSimplify = (orientation == angleOrientation);
if (! isAngleToSimplify)
return false;

double dist = Distance.pointToSegment(p1, p0, p2);
return dist < distanceTol;
}

private static final int NUM_PTS_TO_CHECK = 10;

Expand All @@ -219,18 +209,17 @@ private boolean isShallowSampled(Coordinate p0, Coordinate p2, int i0, int i2, d
if (inc <= 0) inc = 1;

for (int i = i0; i < i2; i += inc) {
if (! isShallow(p0, p2, inputLine[i], distanceTol)) return false;
if (! isShallow(p0, inputLine[i], p2, distanceTol)) return false;
}
return true;
}

private boolean isShallow(Coordinate p0, Coordinate p1, Coordinate p2, double distanceTol)
private static boolean isShallow(Coordinate p0, Coordinate p1, Coordinate p2, double distanceTol)
{
double dist = Distance.pointToSegment(p1, p0, p2);
return dist < distanceTol;
}


private boolean isConcave(Coordinate p0, Coordinate p1, Coordinate p2)
{
int orientation = Orientation.index(p0, p1, p2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -575,8 +575,54 @@ public void testLineClosedNoHole() {
checkBufferHasHole(wkt, 70, false);
}

/**
* See GEOS PR https://github.com/libgeos/geos/pull/978
*/
public void testDefaultBuffer() {
Geometry g = read("POINT (0 0)").buffer(1.0);
Geometry b = g.getBoundary();
Coordinate[] coords = b.getCoordinates();
assertEquals(33, coords.length);
assertEquals(coords[0].x, 1.0);
assertEquals(coords[0].y, 0.0);
assertEquals(coords[8].x, 0.0);
assertEquals(coords[8].y, -1.0);
assertEquals(coords[16].x, -1.0);
assertEquals(coords[16].y, 0.0);
assertEquals(coords[24].x, 0.0);
assertEquals(coords[24].y, 1.0);
}

public void testRingStartSimplified() {
checkBuffer("POLYGON ((200 300, 200 299.9999, 350 100, 30 40, 200 300))",
20, bufParamRoundMitre(5),
"POLYGON ((198.88 334.83, 385.3 86.27, -12.4 11.7, 198.88 334.83))"
);
}

public void testRingEndSimplified() {
checkBuffer("POLYGON ((200 300, 350 100, 30 40, 200 299.9999, 200 300))",
20, bufParamRoundMitre(5),
"POLYGON ((198.88 334.83, 385.3 86.27, -12.4 11.7, 198.88 334.83))"
);
}

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

private static BufferParameters bufParamRoundMitre(double mitreLimit) {
BufferParameters param = new BufferParameters();
param.setJoinStyle(BufferParameters.JOIN_MITRE);
param.setMitreLimit(mitreLimit);
return param;
}

private void checkBuffer(String wkt, double dist, BufferParameters param, String wktExpected) {
Geometry geom = read(wkt);
Geometry result = BufferOp.bufferOp(geom, dist, param);
Geometry expected = read(wktExpected);
checkEqual(expected, result, 0.01);
}

private void checkBufferEmpty(String wkt, double dist, boolean isEmptyExpected) {
Geometry a = read(wkt);
Geometry result = a.buffer(dist);
Expand All @@ -603,22 +649,6 @@ private boolean hasHole(Geometry geom) {
return false;
}

/**
* See GEOS PR https://github.com/libgeos/geos/pull/978
*/
public void testDefaultBuffer() {
Geometry g = read("POINT (0 0)").buffer(1.0);
Geometry b = g.getBoundary();
Coordinate[] coords = b.getCoordinates();
assertEquals(33, coords.length);
assertEquals(coords[0].x, 1.0);
assertEquals(coords[0].y, 0.0);
assertEquals(coords[8].x, 0.0);
assertEquals(coords[8].y, -1.0);
assertEquals(coords[16].x, -1.0);
assertEquals(coords[16].y, 0.0);
assertEquals(coords[24].x, 0.0);
assertEquals(coords[24].y, 1.0);
}


}

0 comments on commit 89bfbab

Please sign in to comment.