convex centroid

pull/1/head
Mike Barry 2021-07-16 20:48:02 -04:00
rodzic 74610f47d1
commit 68842f5b4b
7 zmienionych plików z 271 dodań i 8 usunięć

Wyświetl plik

@ -83,12 +83,12 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
}
}
public Feature validatedPolygon(String layer) {
public Feature centroidIfConvex(String layer) {
try {
return geometry(layer, source.validatedPolygon());
return geometry(layer, source.centroidIfConvex());
} catch (GeometryException e) {
stats.dataError("feature_validated_polygon_" + e.stat());
LOGGER.warn("Error constructing validated polygon for " + source + ": " + e.getMessage());
stats.dataError("feature_centroid_if_convex_" + e.stat());
LOGGER.warn("Error constructing centroid if convex for " + source + ": " + e.getMessage());
return new Feature(layer, EMPTY_GEOM, source.id());
}
}

Wyświetl plik

@ -8,6 +8,7 @@ import java.util.List;
import java.util.Map;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.Lineal;
import org.locationtech.jts.geom.Polygon;
public abstract class SourceFeature {
@ -56,6 +57,24 @@ public abstract class SourceFeature {
worldGeometry().getInteriorPoint());
}
private Geometry centroidIfConvex = null;
private Geometry computeCentroidIfConvex() throws GeometryException {
if (!canBePolygon()) {
return centroid();
} else if (polygon() instanceof Polygon poly &&
poly.getNumInteriorRing() == 0 &&
GeoUtils.isConvex(poly.getExteriorRing())) {
return centroid();
} else {
return pointOnSurface();
}
}
public Geometry centroidIfConvex() throws GeometryException {
return centroidIfConvex != null ? centroidIfConvex : (centroidIfConvex = computeCentroidIfConvex());
}
private Geometry linearGeometry = null;
protected Geometry computeLine() throws GeometryException {

Wyświetl plik

@ -298,6 +298,58 @@ public class GeoUtils {
return JTS_FACTORY.createPolygon(exteriorRing, rings.toArray(LinearRing[]::new));
}
public static boolean isConvex(LinearRing r) {
CoordinateSequence seq = r.getCoordinateSequence();
if (seq.size() <= 3) {
return false;
}
double c0x = seq.getX(0);
double c0y = seq.getY(0);
double c1x = Double.NaN, c1y = Double.NaN;
int i;
for (i = 1; i < seq.size(); i++) {
c1x = seq.getX(i);
c1y = seq.getY(i);
if (c1x != c0x || c1y != c0y) {
break;
}
}
double dx1 = c1x - c0x;
double dy1 = c1y - c0y;
int sign = 0;
for (; i < seq.size(); i++) {
double c2x = seq.getX(i);
double c2y = seq.getY(i);
double dx2 = c2x - c1x;
double dy2 = c2y - c1y;
double z = dx1 * dy2 - dy1 * dx2;
// if z == 0 (with small delta to account for rounding errors) then keep skipping
// points to ignore identical or colinear points
if (Math.abs(z) < 1e-10) {
continue;
}
int s = z >= 0d ? 1 : -1;
if (sign == 0) {
sign = s;
} else if (sign != s) {
return false;
}
c1x = c2x;
c1y = c2y;
dx1 = dx2;
dy1 = dy2;
}
return true;
}
private static record PolyAndArea(Polygon poly, double area) implements Comparable<PolyAndArea> {
PolyAndArea(Polygon poly) {

Wyświetl plik

@ -1,5 +1,7 @@
package com.onthegomap.flatmap;
import static com.onthegomap.flatmap.geo.GeoUtils.JTS_FACTORY;
import static com.onthegomap.flatmap.geo.GeoUtils.coordinateSequence;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -483,4 +485,8 @@ public class TestUtils {
actual.stream().sorted(comparator).toList()
);
}
public static LinearRing newLinearRing(double... coords) {
return JTS_FACTORY.createLinearRing(coordinateSequence(coords));
}
}

Wyświetl plik

@ -9,7 +9,9 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.util.AffineTransformation;
public class GeoUtilsTest {
@ -112,4 +114,190 @@ public class GeoUtilsTest {
public void testMetersPerPixel(int zoom, double meters) {
assertEquals(meters, metersPerPixelAtEquator(zoom), 1);
}
@Test
public void testIsConvexTriangle() {
assertConvex(true, newLinearRing(
0, 0,
1, 0,
0, 1,
0, 0
));
}
@Test
public void testIsConvexRectangle() {
assertConvex(true, newLinearRing(
0, 0,
1, 0,
1, 1,
0, 1,
0, 0
));
}
@Test
public void testBarelyConvexRectangle() {
assertConvex(true, newLinearRing(
0, 0,
1, 0,
1, 1,
0.5, 0.5,
0, 0
));
assertConvex(true, newLinearRing(
0, 0,
1, 0,
1, 1,
0.4, 0.4,
0, 0
));
assertConvex(true, newLinearRing(
0, 0,
1, 0,
1, 1,
0.7, 0.7,
0, 0
));
}
@Test
public void testConcaveRectangleDoublePoints() {
assertConvex(true, newLinearRing(
0, 0,
0, 0,
1, 0,
1, 1,
0, 1,
0, 0
));
assertConvex(true, newLinearRing(
0, 0,
1, 0,
1, 0,
1, 1,
0, 1,
0, 0
));
assertConvex(true, newLinearRing(
0, 0,
1, 0,
1, 1,
1, 1,
0, 1,
0, 0
));
assertConvex(true, newLinearRing(
0, 0,
1, 0,
1, 1,
0, 1,
0, 1,
0, 0
));
assertConvex(true, newLinearRing(
0, 0,
1, 0,
1, 1,
0, 1,
0, 0,
0, 0
));
}
@Test
public void testBarelyConcaveRectangle() {
assertConvex(false, newLinearRing(
0, 0,
1, 0,
1, 1,
0.51, 0.5,
0, 0
));
}
@Test
public void test5PointsConcave() {
assertConvex(false, newLinearRing(
0, 0,
0.5, 0.1,
1, 0,
1, 1,
0, 1,
0, 0
));
assertConvex(false, newLinearRing(
0, 0,
1, 0,
0.9, 0.5,
1, 1,
0, 1,
0, 0
));
assertConvex(false, newLinearRing(
0, 0,
1, 0,
1, 1,
0.5, 0.9,
0, 1,
0, 0
));
assertConvex(false, newLinearRing(
0, 0,
1, 0,
1, 1,
0, 1,
0.1, 0.5,
0, 0
));
}
@Test
public void test5PointsColinear() {
assertConvex(true, newLinearRing(
0, 0,
0.5, 0,
1, 0,
1, 1,
0, 1,
0, 0
));
assertConvex(true, newLinearRing(
0, 0,
1, 0,
1, 0.5,
1, 1,
0, 1,
0, 0
));
assertConvex(true, newLinearRing(
0, 0,
1, 0,
1, 1,
0.5, 1,
0, 1,
0, 0
));
assertConvex(true, newLinearRing(
0, 0,
1, 0,
1, 1,
0, 1,
0, 0.5,
0, 0
));
}
private static void assertConvex(boolean isConvex, LinearRing ring) {
for (double rotation : new double[]{0, 90, 180, 270}) {
LinearRing rotated = (LinearRing) AffineTransformation.rotationInstance(Math.toRadians(rotation)).transform(ring);
for (boolean flip : new boolean[]{false, true}) {
LinearRing flipped = flip ? (LinearRing) AffineTransformation.scaleInstance(-1, 1).transform(rotated) : rotated;
for (boolean reverse : new boolean[]{false, true}) {
LinearRing reversed = reverse ? flipped.reverse() : flipped;
assertEquals(isConvex, isConvex(reversed), "rotation=" + rotation + " flip=" + flip + " reverse=" + reverse);
}
}
}
}
}

Wyświetl plik

@ -14,8 +14,7 @@ public class Housenumber implements OpenMapTilesSchema.Housenumber, Tables.OsmHo
@Override
public void process(Tables.OsmHousenumberPoint element, FeatureCollector features) {
// TODO if not convex, use pointOnSurface
features.centroid(LAYER_NAME)
features.centroidIfConvex(LAYER_NAME)
.setBufferPixels(BUFFER_SIZE)
.setAttr(Fields.HOUSENUMBER, element.housenumber())
.setZoomRange(14, 14);

Wyświetl plik

@ -87,8 +87,7 @@ public class Poi implements OpenMapTilesSchema.Poi,
@Override
public void process(Tables.OsmPoiPolygon element, FeatureCollector features) {
// TODO pointOnSurface if not convex
setupPoiFeature(element, features.centroid(LAYER_NAME));
setupPoiFeature(element, features.centroidIfConvex(LAYER_NAME));
}
private <T extends Tables.WithSubclass & Tables.WithStation & Tables.WithFunicular & Tables.WithSport & Tables.WithInformation & Tables.WithReligion & Tables.WithMappingKey & Tables.WithName & Tables.WithIndoor & Tables.WithLayer & Tables.WithSource>