kopia lustrzana https://github.com/onthegomap/planetiler
Improve is convex logic (#459)
rodzic
4f43f1127a
commit
723f44e236
|
@ -408,8 +408,11 @@ public class GeoUtils {
|
||||||
* {@code ring}, ignoring repeated and collinear points.
|
* {@code ring}, ignoring repeated and collinear points.
|
||||||
*/
|
*/
|
||||||
public static boolean isConvex(LinearRing ring) {
|
public static boolean isConvex(LinearRing ring) {
|
||||||
|
double threshold = 1e-3;
|
||||||
|
double minPointsToCheck = 10;
|
||||||
CoordinateSequence seq = ring.getCoordinateSequence();
|
CoordinateSequence seq = ring.getCoordinateSequence();
|
||||||
if (seq.size() <= 3) {
|
int size = seq.size();
|
||||||
|
if (size <= 3) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -418,7 +421,7 @@ public class GeoUtils {
|
||||||
double c0y = seq.getY(0);
|
double c0y = seq.getY(0);
|
||||||
double c1x = Double.NaN, c1y = Double.NaN;
|
double c1x = Double.NaN, c1y = Double.NaN;
|
||||||
int i;
|
int i;
|
||||||
for (i = 1; i < seq.size(); i++) {
|
for (i = 1; i < size; i++) {
|
||||||
c1x = seq.getX(i);
|
c1x = seq.getX(i);
|
||||||
c1y = seq.getY(i);
|
c1y = seq.getY(i);
|
||||||
if (c1x != c0x || c1y != c0y) {
|
if (c1x != c0x || c1y != c0y) {
|
||||||
|
@ -429,29 +432,39 @@ public class GeoUtils {
|
||||||
double dx1 = c1x - c0x;
|
double dx1 = c1x - c0x;
|
||||||
double dy1 = c1y - c0y;
|
double dy1 = c1y - c0y;
|
||||||
|
|
||||||
int sign = 0;
|
double negZ = 1e-20, posZ = 1e-20;
|
||||||
|
|
||||||
for (; i < seq.size(); i++) {
|
// need to wrap around to make sure the triangle formed by last and first points does not change sign
|
||||||
double c2x = seq.getX(i);
|
for (; i <= size + 1; i++) {
|
||||||
double c2y = seq.getY(i);
|
// first and last point should be the same, so skip index 0
|
||||||
|
int idx = i < size ? i : (i + 1 - size);
|
||||||
|
double c2x = seq.getX(idx);
|
||||||
|
double c2y = seq.getY(idx);
|
||||||
|
|
||||||
double dx2 = c2x - c1x;
|
double dx2 = c2x - c1x;
|
||||||
double dy2 = c2y - c1y;
|
double dy2 = c2y - c1y;
|
||||||
double z = dx1 * dy2 - dy1 * dx2;
|
double z = dx1 * dy2 - dy1 * dx2;
|
||||||
|
|
||||||
// if z == 0 (with small delta to account for rounding errors) then keep skipping
|
double absZ = Math.abs(z);
|
||||||
// points to ignore repeated or collinear points
|
|
||||||
if (Math.abs(z) < 1e-10) {
|
// look for sign changes in the triangles formed by sequential points
|
||||||
continue;
|
// but, we want to allow for rounding errors and small concavities relative to the overall shape
|
||||||
|
// so track the largest positive and negative threshold for triangle area and compare them once we
|
||||||
|
// have enough points
|
||||||
|
boolean extendedBounds = false;
|
||||||
|
if (z < 0 && absZ > negZ) {
|
||||||
|
negZ = absZ;
|
||||||
|
extendedBounds = true;
|
||||||
|
} else if (z > 0 && absZ > posZ) {
|
||||||
|
posZ = absZ;
|
||||||
|
extendedBounds = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int s = z >= 0d ? 1 : -1;
|
if (i == minPointsToCheck || (i > minPointsToCheck && extendedBounds)) {
|
||||||
if (sign == 0) {
|
double ratio = negZ < posZ ? negZ / posZ : posZ / negZ;
|
||||||
// on the first non-repeated, non-collinear points, store sign of the area for comparison
|
if (ratio > threshold) {
|
||||||
sign = s;
|
return false;
|
||||||
} else if (sign != s) {
|
}
|
||||||
// the sign of this triangle has changed, not convex
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c1x = c2x;
|
c1x = c2x;
|
||||||
|
@ -459,7 +472,7 @@ public class GeoUtils {
|
||||||
dx1 = dx2;
|
dx1 = dx2;
|
||||||
dy1 = dy2;
|
dy1 = dy2;
|
||||||
}
|
}
|
||||||
return true;
|
return (negZ < posZ ? negZ / posZ : posZ / negZ) < threshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -206,7 +206,7 @@ class GeoUtilsTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testBarelyConcaveRectangle() {
|
void testBarelyConcaveTriangle() {
|
||||||
assertConvex(false, newLinearRing(
|
assertConvex(false, newLinearRing(
|
||||||
0, 0,
|
0, 0,
|
||||||
1, 0,
|
1, 0,
|
||||||
|
@ -216,6 +216,24 @@ class GeoUtilsTest {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAllowVerySmallConcavity() {
|
||||||
|
assertConvex(true, newLinearRing(
|
||||||
|
0, 0,
|
||||||
|
1, 0,
|
||||||
|
1, 1,
|
||||||
|
0.5001, 0.5,
|
||||||
|
0, 0
|
||||||
|
));
|
||||||
|
assertConvex(true, newLinearRing(
|
||||||
|
0, 0,
|
||||||
|
1, 0,
|
||||||
|
1, 1,
|
||||||
|
0.5, 0.4999,
|
||||||
|
0, 0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void test5PointsConcave() {
|
void test5PointsConcave() {
|
||||||
assertConvex(false, newLinearRing(
|
assertConvex(false, newLinearRing(
|
||||||
|
@ -295,7 +313,11 @@ class GeoUtilsTest {
|
||||||
LinearRing flipped = flip ? (LinearRing) AffineTransformation.scaleInstance(-1, 1).transform(rotated) : rotated;
|
LinearRing flipped = flip ? (LinearRing) AffineTransformation.scaleInstance(-1, 1).transform(rotated) : rotated;
|
||||||
for (boolean reverse : new boolean[]{false, true}) {
|
for (boolean reverse : new boolean[]{false, true}) {
|
||||||
LinearRing reversed = reverse ? flipped.reverse() : flipped;
|
LinearRing reversed = reverse ? flipped.reverse() : flipped;
|
||||||
assertEquals(isConvex, isConvex(reversed), "rotation=" + rotation + " flip=" + flip + " reverse=" + reverse);
|
for (double scale : new double[]{1, 1e-2, 1 / Math.pow(2, 14) / 4096}) {
|
||||||
|
LinearRing scaled = (LinearRing) AffineTransformation.scaleInstance(scale, scale).transform(reversed);
|
||||||
|
assertEquals(isConvex, isConvex(scaled),
|
||||||
|
"rotation=" + rotation + " flip=" + flip + " reverse=" + reverse + " scale=" + scale);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue