From 2d3dfa95774ce517ed2527d9f98283d8d707d928 Mon Sep 17 00:00:00 2001 From: Mike Barry Date: Tue, 25 May 2021 06:05:41 -0400 Subject: [PATCH] reader geometry coercion --- .../onthegomap/flatmap/FeatureCollector.java | 77 +++- .../com/onthegomap/flatmap/GeometryType.java | 48 +++ .../com/onthegomap/flatmap/SourceFeature.java | 69 +++- .../com/onthegomap/flatmap/geo/GeoUtils.java | 35 ++ .../flatmap/read/NaturalEarthReader.java | 6 +- .../flatmap/read/OpenStreetMapReader.java | 37 +- .../com/onthegomap/flatmap/read/Reader.java | 3 +- .../flatmap/read/ReaderFeature.java | 11 + .../flatmap/read/ShapefileReader.java | 5 +- .../flatmap/FeatureCollectorTest.java | 358 +++++++++++++++++- .../com/onthegomap/flatmap/TestUtils.java | 6 +- .../onthegomap/flatmap/geo/GeoUtilsTest.java | 71 ++++ 12 files changed, 685 insertions(+), 41 deletions(-) create mode 100644 src/main/java/com/onthegomap/flatmap/GeometryType.java diff --git a/src/main/java/com/onthegomap/flatmap/FeatureCollector.java b/src/main/java/com/onthegomap/flatmap/FeatureCollector.java index 66562662..82130428 100644 --- a/src/main/java/com/onthegomap/flatmap/FeatureCollector.java +++ b/src/main/java/com/onthegomap/flatmap/FeatureCollector.java @@ -1,15 +1,22 @@ package com.onthegomap.flatmap; import com.onthegomap.flatmap.collections.CacheByZoom; +import com.onthegomap.flatmap.geo.GeoUtils; +import com.onthegomap.flatmap.geo.GeometryException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; import org.locationtech.jts.geom.Geometry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class FeatureCollector implements Iterable { + private static final Geometry EMPTY_GEOM = GeoUtils.JTS_FACTORY.createGeometryCollection(); + private static final Logger LOGGER = LoggerFactory.getLogger(FeatureCollector.class); + private final SourceFeature source; private final List output = new ArrayList<>(); private final CommonParams config; @@ -24,22 +31,58 @@ public class FeatureCollector implements Iterable { return output.iterator(); } + public Feature geometry(String layer, Geometry geometry) { + Feature feature = new Feature(layer, geometry); + output.add(feature); + return feature; + } + public Feature point(String layer) { - var feature = new Feature(layer, source.isPoint() ? source.worldGeometry() : source.centroid(), false); - output.add(feature); - return feature; + try { + if (!source.isPoint()) { + throw new GeometryException("not a point"); + } + return geometry(layer, source.worldGeometry()); + } catch (GeometryException e) { + LOGGER.warn("Error getting point geometry for " + source + ": " + e); + return new Feature(layer, EMPTY_GEOM); + } } - public Feature line(String layername) { - var feature = new Feature(layername, source.line(), false); - output.add(feature); - return feature; + public Feature centroid(String layer) { + try { + return geometry(layer, source.centroid()); + } catch (GeometryException e) { + LOGGER.warn("Error getting centroid for " + source + ": " + e); + return new Feature(layer, EMPTY_GEOM); + } } - public Feature polygon(String layername) { - var feature = new Feature(layername, source.polygon(), true); - output.add(feature); - return feature; + public Feature line(String layer) { + try { + return geometry(layer, source.line()); + } catch (GeometryException e) { + LOGGER.warn("Error constructing line for " + source + ": " + e); + return new Feature(layer, EMPTY_GEOM); + } + } + + public Feature polygon(String layer) { + try { + return geometry(layer, source.polygon()); + } catch (GeometryException e) { + LOGGER.warn("Error constructing polygon for " + source + ": " + e); + return new Feature(layer, EMPTY_GEOM); + } + } + + public Feature pointOnSurface(String layer) { + try { + return geometry(layer, source.pointOnSurface()); + } catch (GeometryException e) { + LOGGER.warn("Error constructing point on surface for " + source + ": " + e); + return new Feature(layer, EMPTY_GEOM); + } } public static record Factory(CommonParams config) { @@ -51,12 +94,12 @@ public class FeatureCollector implements Iterable { public final class Feature { - private final boolean area; private static final double DEFAULT_LABEL_GRID_SIZE = 0; private static final int DEFAULT_LABEL_GRID_LIMIT = 0; private final String layer; private final Geometry geom; private final Map attrs = new TreeMap<>(); + private final GeometryType geometryType; private int zOrder; private int minzoom = config.minzoom(); private int maxzoom = config.maxzoom(); @@ -73,11 +116,11 @@ public class FeatureCollector implements Iterable { private double pixelToleranceAtMaxZoom = 256d / 4096; private ZoomFunction pixelTolerance = null; - private Feature(String layer, Geometry geom, boolean area) { + private Feature(String layer, Geometry geom) { this.layer = layer; this.geom = geom; this.zOrder = 0; - this.area = area; + this.geometryType = GeometryType.valueOf(geom); } public int getZorder() { @@ -254,8 +297,12 @@ public class FeatureCollector implements Iterable { return setAttr(key, ZoomFunction.minZoom(minzoom, value)); } + public GeometryType getGeometryType() { + return geometryType; + } + public boolean area() { - return area; + return geometryType == GeometryType.POLYGON; } @Override diff --git a/src/main/java/com/onthegomap/flatmap/GeometryType.java b/src/main/java/com/onthegomap/flatmap/GeometryType.java new file mode 100644 index 00000000..a7910360 --- /dev/null +++ b/src/main/java/com/onthegomap/flatmap/GeometryType.java @@ -0,0 +1,48 @@ +package com.onthegomap.flatmap; + +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.Lineal; +import org.locationtech.jts.geom.Polygonal; +import org.locationtech.jts.geom.Puntal; +import vector_tile.VectorTile; + +public enum GeometryType { + UNKNOWN(VectorTile.Tile.GeomType.UNKNOWN), + POINT(VectorTile.Tile.GeomType.POINT), + LINE(VectorTile.Tile.GeomType.LINESTRING), + POLYGON(VectorTile.Tile.GeomType.POLYGON); + + private final VectorTile.Tile.GeomType protobufType; + + GeometryType(VectorTile.Tile.GeomType protobufType) { + this.protobufType = protobufType; + } + + public static GeometryType valueOf(Geometry geom) { + return geom instanceof Puntal ? POINT + : geom instanceof Lineal ? LINE + : geom instanceof Polygonal ? POLYGON + : UNKNOWN; + } + + public static GeometryType valueOf(VectorTile.Tile.GeomType geomType) { + return switch (geomType) { + case POINT -> POINT; + case LINESTRING -> LINE; + case POLYGON -> POLYGON; + default -> UNKNOWN; + }; + } + + public static GeometryType valueOf(byte val) { + return valueOf(VectorTile.Tile.GeomType.forNumber(val)); + } + + public byte asByte() { + return (byte) protobufType.getNumber(); + } + + public VectorTile.Tile.GeomType asProtobufType() { + return protobufType; + } +} diff --git a/src/main/java/com/onthegomap/flatmap/SourceFeature.java b/src/main/java/com/onthegomap/flatmap/SourceFeature.java index 00b195bb..df2e9ea1 100644 --- a/src/main/java/com/onthegomap/flatmap/SourceFeature.java +++ b/src/main/java/com/onthegomap/flatmap/SourceFeature.java @@ -1,19 +1,24 @@ package com.onthegomap.flatmap; +import com.onthegomap.flatmap.geo.GeoUtils; +import com.onthegomap.flatmap.geo.GeometryException; import java.util.Map; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.Lineal; public abstract class SourceFeature { private final Map properties; + private String source; + private String sourceLayer; protected SourceFeature(Map properties) { this.properties = properties; } - public abstract Geometry latLonGeometry(); + public abstract Geometry latLonGeometry() throws GeometryException; - public abstract Geometry worldGeometry(); + public abstract Geometry worldGeometry() throws GeometryException; public void setTag(String key, Object value) { properties.put(key, value); @@ -25,32 +30,54 @@ public abstract class SourceFeature { private Geometry centroid = null; - public Geometry centroid() { - return centroid != null ? centroid : (centroid = worldGeometry().getCentroid()); + public Geometry centroid() throws GeometryException { + return centroid != null ? centroid : (centroid = + canBePolygon() ? polygon().getCentroid() : + canBeLine() ? line().getCentroid() : + worldGeometry().getCentroid()); + } + + private Geometry pointOnSurface = null; + + public Geometry pointOnSurface() throws GeometryException { + return pointOnSurface != null ? pointOnSurface : (pointOnSurface = + canBePolygon() ? polygon().getInteriorPoint() : + canBeLine() ? line().getInteriorPoint() : + worldGeometry().getInteriorPoint()); } private Geometry linearGeometry = null; - public Geometry line() { - return linearGeometry != null ? linearGeometry : (linearGeometry = worldGeometry()); + public Geometry line() throws GeometryException { + if (!canBeLine()) { + throw new GeometryException("cannot be line"); + } + if (linearGeometry == null) { + Geometry world = worldGeometry(); + linearGeometry = world instanceof Lineal ? world : GeoUtils.polygonToLineString(world); + } + return linearGeometry; } private Geometry polygonGeometry = null; - public Geometry polygon() { + public Geometry polygon() throws GeometryException { + if (!canBePolygon()) { + throw new GeometryException("cannot be polygon"); + } return polygonGeometry != null ? polygonGeometry : (polygonGeometry = worldGeometry()); } private double area = Double.NaN; - public double area() { - return Double.isNaN(area) ? (area = polygon().getArea()) : area; + public double area() throws GeometryException { + return Double.isNaN(area) ? (area = canBePolygon() ? polygon().getArea() : 0) : area; } private double length = Double.NaN; - public double length() { - return Double.isNaN(length) ? (length = line().getLength()) : length; + public double length() throws GeometryException { + return Double.isNaN(length) ? (length = worldGeometry().getLength()) : length; } public Object getTag(String key) { @@ -79,4 +106,24 @@ public abstract class SourceFeature { } public abstract boolean isPoint(); + + public abstract boolean canBePolygon(); + + public abstract boolean canBeLine(); + + public String getSource() { + return source; + } + + public String getSourceLayer() { + return sourceLayer; + } + + public void setSource(String source) { + this.source = source; + } + + public void setSourceLayer(String sourceLayer) { + this.sourceLayer = sourceLayer; + } } diff --git a/src/main/java/com/onthegomap/flatmap/geo/GeoUtils.java b/src/main/java/com/onthegomap/flatmap/geo/GeoUtils.java index 56c85d0b..926b442a 100644 --- a/src/main/java/com/onthegomap/flatmap/geo/GeoUtils.java +++ b/src/main/java/com/onthegomap/flatmap/geo/GeoUtils.java @@ -1,5 +1,6 @@ package com.onthegomap.flatmap.geo; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.locationtech.jts.geom.Coordinate; @@ -7,10 +8,13 @@ import org.locationtech.jts.geom.CoordinateSequence; import org.locationtech.jts.geom.CoordinateXY; import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.LinearRing; import org.locationtech.jts.geom.MultiPoint; import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; import org.locationtech.jts.geom.PrecisionModel; import org.locationtech.jts.geom.impl.PackedCoordinateSequence; import org.locationtech.jts.geom.util.GeometryTransformer; @@ -240,4 +244,35 @@ public class GeoUtils { public static Geometry createMultiPoint(List points) { return JTS_FACTORY.createMultiPoint(points.toArray(EMPTY_POINT_ARRAY)); } + + public static Geometry polygonToLineString(Geometry world) throws GeometryException { + List lineStrings = new ArrayList<>(); + getLineStrings(world, lineStrings); + if (lineStrings.size() == 0) { + throw new GeometryException("No line strings"); + } else if (lineStrings.size() == 1) { + return lineStrings.get(0); + } else { + return createMultiLineString(lineStrings); + } + } + + private static void getLineStrings(Geometry input, List output) throws GeometryException { + if (input instanceof LinearRing linearRing) { + output.add(JTS_FACTORY.createLineString(linearRing.getCoordinateSequence())); + } else if (input instanceof LineString lineString) { + output.add(lineString); + } else if (input instanceof Polygon polygon) { + getLineStrings(polygon.getExteriorRing(), output); + for (int i = 0; i < polygon.getNumInteriorRing(); i++) { + getLineStrings(polygon.getInteriorRingN(i), output); + } + } else if (input instanceof GeometryCollection gc) { + for (int i = 0; i < gc.getNumGeometries(); i++) { + getLineStrings(gc.getGeometryN(i), output); + } + } else { + throw new GeometryException("unrecognized geometry type: " + input.getGeometryType()); + } + } } diff --git a/src/main/java/com/onthegomap/flatmap/read/NaturalEarthReader.java b/src/main/java/com/onthegomap/flatmap/read/NaturalEarthReader.java index a48b227f..04512970 100644 --- a/src/main/java/com/onthegomap/flatmap/read/NaturalEarthReader.java +++ b/src/main/java/com/onthegomap/flatmap/read/NaturalEarthReader.java @@ -3,7 +3,6 @@ package com.onthegomap.flatmap.read; import com.onthegomap.flatmap.CommonParams; import com.onthegomap.flatmap.FileUtils; import com.onthegomap.flatmap.Profile; -import com.onthegomap.flatmap.SourceFeature; import com.onthegomap.flatmap.collections.FeatureGroup; import com.onthegomap.flatmap.geo.GeoUtils; import com.onthegomap.flatmap.monitoring.Stats; @@ -104,7 +103,7 @@ public class NaturalEarthReader extends Reader { } @Override - public Topology.SourceStep read() { + public Topology.SourceStep read() { return next -> { var tables = tableNames(); for (int i = 0; i < tables.size(); i++) { @@ -129,7 +128,8 @@ public class NaturalEarthReader extends Reader { continue; } Geometry latLonGeometry = GeoUtils.wkbReader.read(geometry); - SourceFeature readerGeometry = new ReaderFeature(latLonGeometry, column.length - 1); + ReaderFeature readerGeometry = new ReaderFeature(latLonGeometry, column.length - 1); + readerGeometry.setSourceLayer(table); for (int c = 0; c < column.length; c++) { if (c != geometryColumn) { Object value = rs.getObject(c + 1); diff --git a/src/main/java/com/onthegomap/flatmap/read/OpenStreetMapReader.java b/src/main/java/com/onthegomap/flatmap/read/OpenStreetMapReader.java index 97eb9b01..f18cc10a 100644 --- a/src/main/java/com/onthegomap/flatmap/read/OpenStreetMapReader.java +++ b/src/main/java/com/onthegomap/flatmap/read/OpenStreetMapReader.java @@ -137,7 +137,10 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima feature = new NodeSourceFeature(node); } else if (readerElement instanceof ReaderWay way) { waysProcessed.incrementAndGet(); - feature = new WaySourceFeature(way, nodeCache); + LongArrayList nodes = way.getNodes(); + boolean closed = nodes.size() > 1 && nodes.get(0) == nodes.get(nodes.size() - 1); + String area = way.getTag("area"); + feature = new WaySourceFeature(way, closed, area, nodeCache); } else if (readerElement instanceof ReaderRelation rel) { // ensure all ways finished processing before we start relations if (waysDone.getCount() > 0) { @@ -210,8 +213,15 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima private static abstract class ProxyFeature extends SourceFeature { - public ProxyFeature(ReaderElement elem) { + private final boolean polygon; + private final boolean line; + private final boolean point; + + public ProxyFeature(ReaderElement elem, boolean point, boolean line, boolean polygon) { super(ReaderElementUtils.getProperties(elem)); + this.point = point; + this.line = line; + this.polygon = polygon; } private Geometry latLonGeom; @@ -229,6 +239,21 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima } protected abstract Geometry computeWorldGeometry(); + + @Override + public boolean isPoint() { + return point; + } + + @Override + public boolean canBeLine() { + return line; + } + + @Override + public boolean canBePolygon() { + return polygon; + } } private static class NodeSourceFeature extends ProxyFeature { @@ -237,7 +262,7 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima private final double lat; NodeSourceFeature(ReaderNode node) { - super(node); + super(node, true, false, false); this.lon = node.getLon(); this.lat = node.getLat(); } @@ -261,8 +286,8 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima private final NodeGeometryCache nodeCache; private final LongArrayList nodeIds; - public WaySourceFeature(ReaderWay way, NodeGeometryCache nodeCache) { - super(way); + public WaySourceFeature(ReaderWay way, boolean closed, String area, NodeGeometryCache nodeCache) { + super(way, false, !closed || !"yes".equals(area), closed && !"no".equals(area)); this.nodeIds = way.getNodes(); this.nodeCache = nodeCache; } @@ -281,7 +306,7 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima private static class MultipolygonSourceFeature extends ProxyFeature { public MultipolygonSourceFeature(ReaderRelation relation) { - super(relation); + super(relation, false, false, true); } @Override diff --git a/src/main/java/com/onthegomap/flatmap/read/Reader.java b/src/main/java/com/onthegomap/flatmap/read/Reader.java index ee786978..d966ed95 100644 --- a/src/main/java/com/onthegomap/flatmap/read/Reader.java +++ b/src/main/java/com/onthegomap/flatmap/read/Reader.java @@ -47,6 +47,7 @@ public abstract class Reader implements Closeable { ); while ((sourceFeature = prev.get()) != null) { featuresRead.incrementAndGet(); + sourceFeature.setSource(name); FeatureCollector features = featureCollectors.get(sourceFeature); if (sourceFeature.latLonGeometry().getEnvelopeInternal().intersects(latLonBounds)) { profile.processFeature(sourceFeature, features); @@ -74,7 +75,7 @@ public abstract class Reader implements Closeable { public abstract long getCount(); - public abstract Topology.SourceStep read(); + public abstract Topology.SourceStep read(); @Override public abstract void close(); diff --git a/src/main/java/com/onthegomap/flatmap/read/ReaderFeature.java b/src/main/java/com/onthegomap/flatmap/read/ReaderFeature.java index 9705e4e1..42db6102 100644 --- a/src/main/java/com/onthegomap/flatmap/read/ReaderFeature.java +++ b/src/main/java/com/onthegomap/flatmap/read/ReaderFeature.java @@ -6,6 +6,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.Polygonal; import org.locationtech.jts.geom.Puntal; public class ReaderFeature extends SourceFeature { @@ -44,6 +45,16 @@ public class ReaderFeature extends SourceFeature { return latLonGeometry instanceof Puntal; } + @Override + public boolean canBePolygon() { + return latLonGeometry instanceof Polygonal; + } + + @Override + public boolean canBeLine() { + return !isPoint(); + } + @Override public boolean equals(Object obj) { if (obj == this) { diff --git a/src/main/java/com/onthegomap/flatmap/read/ShapefileReader.java b/src/main/java/com/onthegomap/flatmap/read/ShapefileReader.java index 49ebc6ee..0e667989 100644 --- a/src/main/java/com/onthegomap/flatmap/read/ShapefileReader.java +++ b/src/main/java/com/onthegomap/flatmap/read/ShapefileReader.java @@ -3,7 +3,6 @@ package com.onthegomap.flatmap.read; import com.onthegomap.flatmap.CommonParams; import com.onthegomap.flatmap.FileUtils; import com.onthegomap.flatmap.Profile; -import com.onthegomap.flatmap.SourceFeature; import com.onthegomap.flatmap.collections.FeatureGroup; import com.onthegomap.flatmap.monitoring.Stats; import com.onthegomap.flatmap.worker.Topology; @@ -103,7 +102,7 @@ public class ShapefileReader extends Reader implements Closeable { } @Override - public Topology.SourceStep read() { + public Topology.SourceStep read() { return next -> { try (var iter = inputSource.features()) { while (iter.hasNext()) { @@ -114,7 +113,7 @@ public class ShapefileReader extends Reader implements Closeable { latLonGeometry = JTS.transform(source, transformToLatLon); } if (latLonGeometry != null) { - SourceFeature geom = new ReaderFeature(latLonGeometry, attributeNames.length); + ReaderFeature geom = new ReaderFeature(latLonGeometry, attributeNames.length); for (int i = 1; i < attributeNames.length; i++) { geom.setTag(attributeNames[i], feature.getAttribute(i)); } diff --git a/src/test/java/com/onthegomap/flatmap/FeatureCollectorTest.java b/src/test/java/com/onthegomap/flatmap/FeatureCollectorTest.java index ebcbd3b8..1f4e4b50 100644 --- a/src/test/java/com/onthegomap/flatmap/FeatureCollectorTest.java +++ b/src/test/java/com/onthegomap/flatmap/FeatureCollectorTest.java @@ -1,17 +1,29 @@ package com.onthegomap.flatmap; import static com.onthegomap.flatmap.TestUtils.assertSubmap; +import static com.onthegomap.flatmap.TestUtils.newCoordinateList; import static com.onthegomap.flatmap.TestUtils.newLineString; +import static com.onthegomap.flatmap.TestUtils.newMultiLineString; +import static com.onthegomap.flatmap.TestUtils.newMultiPolygon; import static com.onthegomap.flatmap.TestUtils.newPoint; import static com.onthegomap.flatmap.TestUtils.newPolygon; import static com.onthegomap.flatmap.TestUtils.rectangle; +import static com.onthegomap.flatmap.TestUtils.rectangleCoordList; +import static com.onthegomap.flatmap.TestUtils.round; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import com.onthegomap.flatmap.geo.GeoUtils; +import com.onthegomap.flatmap.geo.GeometryException; import com.onthegomap.flatmap.read.ReaderFeature; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.StreamSupport; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; public class FeatureCollectorTest { @@ -247,5 +259,349 @@ public class FeatureCollectorTest { assertEquals(2d, poly.getPixelTolerance(14)); } - // TODO test shape coercion + /* + * SHAPE COERCION TESTS + */ + @Test + public void testPointReaderFeatureCoercion() throws GeometryException { + var pointSourceFeature = new ReaderFeature(newPoint(0, 0), Map.of()); + assertEquals(0, pointSourceFeature.area()); + assertEquals(0, pointSourceFeature.length()); + + var fc = factory.get(pointSourceFeature); + fc.line("layer").setZoomRange(0, 10); + fc.polygon("layer").setZoomRange(0, 10); + assertFalse(fc.iterator().hasNext(), "silently fail coercing to line/polygon"); + fc.point("layer").setZoomRange(0, 10); + fc.centroid("layer").setZoomRange(0, 10); + fc.pointOnSurface("layer").setZoomRange(0, 10); + var iter = fc.iterator(); + for (int i = 0; i < 3; i++) { + assertTrue(iter.hasNext(), "item " + i); + var item = iter.next(); + assertEquals(GeometryType.POINT, item.getGeometryType()); + assertEquals(newPoint(0.5, 0.5), item.getGeometry()); + } + + assertFalse(iter.hasNext()); + } + + @ParameterizedTest + @ValueSource(ints = {2, 3, 4}) + public void testLineWithSamePointsReaderFeatureCoercion(int nPoints) throws GeometryException { + double[] coords = new double[nPoints * 2]; + Arrays.fill(coords, 0d); + double[] worldCoords = new double[nPoints * 2]; + Arrays.fill(worldCoords, 0.5d); + var sourceLine = new ReaderFeature(newLineString(coords), Map.of()); + assertEquals(0, sourceLine.length()); + assertEquals(0, sourceLine.area()); + + var fc = factory.get(sourceLine); + fc.point("layer").setZoomRange(0, 10); + fc.polygon("layer").setZoomRange(0, 10); + assertFalse(fc.iterator().hasNext(), "silently fail coercing to point/polygon"); + fc.line("layer").setZoomRange(0, 10); + fc.centroid("layer").setZoomRange(0, 10); + fc.pointOnSurface("layer").setZoomRange(0, 10); + var iter = fc.iterator(); + + var item = iter.next(); + assertEquals(GeometryType.LINE, item.getGeometryType()); + assertEquals(newLineString(worldCoords), item.getGeometry()); + + item = iter.next(); + assertEquals(GeometryType.POINT, item.getGeometryType()); + assertEquals(newPoint(0.5, 0.5), item.getGeometry()); + + item = iter.next(); + assertEquals(GeometryType.POINT, item.getGeometryType()); + assertEquals(newPoint(0.5, 0.5), item.getGeometry()); + + assertFalse(iter.hasNext()); + } + + private static double[] worldToLatLon(double... coords) { + double[] result = new double[coords.length]; + for (int i = 0; i < coords.length; i += 2) { + result[i] = GeoUtils.getWorldLon(coords[i]); + result[i + 1] = GeoUtils.getWorldLat(coords[i + 1]); + } + return result; + } + + @Test + public void testNonZeroLineStringReaderFeatureCoercion() throws GeometryException { + var sourceLine = new ReaderFeature(newLineString(worldToLatLon( + 0.2, 0.2, + 0.75, 0.75, + 0.25, 0.75, + 0.2, 0.2 + )), Map.of()); + assertEquals(0, sourceLine.area()); + assertEquals(1.83008, sourceLine.length(), 1e-5); + + var fc = factory.get(sourceLine); + fc.point("layer").setZoomRange(0, 10); + fc.polygon("layer").setZoomRange(0, 10); + + assertFalse(fc.iterator().hasNext(), "silently fail coercing to point/polygon"); + fc.line("layer").setZoomRange(0, 10); + fc.centroid("layer").setZoomRange(0, 10); + fc.pointOnSurface("layer").setZoomRange(0, 10); + var iter = fc.iterator(); + + var item = iter.next(); + assertEquals(GeometryType.LINE, item.getGeometryType()); + assertEquals(round(newLineString( + 0.2, 0.2, + 0.75, 0.75, + 0.25, 0.75, + 0.2, 0.2 + )), round(item.getGeometry())); + + item = iter.next(); + assertEquals(GeometryType.POINT, item.getGeometryType()); + assertEquals(round(newPoint(0.40639, 0.55013)), round(item.getGeometry()), "centroid"); + + item = iter.next(); + assertEquals(GeometryType.POINT, item.getGeometryType()); + assertEquals(newPoint(0.25, 0.75), item.getGeometry(), "point on surface"); + + assertFalse(iter.hasNext()); + } + + @Test + public void testPolygonReaderFeatureCoercion() throws GeometryException { + var sourceLine = new ReaderFeature(newPolygon(worldToLatLon( + 0.25, 0.25, + 0.75, 0.75, + 0.25, 0.75, + 0.25, 0.25 + )), Map.of()); + assertEquals(0.125, sourceLine.area()); + assertEquals(1.7071067811865475, sourceLine.length(), 1e-5); + + var fc = factory.get(sourceLine); + fc.point("layer").setZoomRange(0, 10); + assertFalse(fc.iterator().hasNext(), "silently fail coercing to point"); + + fc.polygon("layer").setZoomRange(0, 10); + fc.line("layer").setZoomRange(0, 10); + fc.centroid("layer").setZoomRange(0, 10); + fc.pointOnSurface("layer").setZoomRange(0, 10); + var iter = fc.iterator(); + + var item = iter.next(); + assertEquals(GeometryType.POLYGON, item.getGeometryType()); + assertEquals(round(newPolygon( + 0.25, 0.25, + 0.75, 0.75, + 0.25, 0.75, + 0.25, 0.25 + )), round(item.getGeometry())); + + item = iter.next(); + assertEquals(GeometryType.LINE, item.getGeometryType()); + assertEquals(round(newLineString( + 0.25, 0.25, + 0.75, 0.75, + 0.25, 0.75, + 0.25, 0.25 + )), round(item.getGeometry())); + + item = iter.next(); + assertEquals(GeometryType.POINT, item.getGeometryType()); + assertEquals(round(newPoint(0.4166667, 0.5833333)), round(item.getGeometry())); + + item = iter.next(); + assertEquals(GeometryType.POINT, item.getGeometryType()); + assertEquals(round(newPoint(0.375, 0.5)), round(item.getGeometry())); + + assertFalse(iter.hasNext()); + } + + @Test + public void testPolygonWithHoleCoercion() throws GeometryException { + var sourceLine = new ReaderFeature(newPolygon(newCoordinateList(worldToLatLon( + 0, 0, + 1, 0, + 1, 1, + 0, 1, + 0, 0 + )), List.of(newCoordinateList(worldToLatLon( + 0.25, 0.25, + 0.75, 0.25, + 0.75, 0.75, + 0.25, 0.75, + 0.25, 0.25 + )))), Map.of()); + assertEquals(0.75, sourceLine.area(), 1e-5); + assertEquals(6, sourceLine.length(), 1e-5); + + var fc = factory.get(sourceLine); + fc.point("layer").setZoomRange(0, 10); + assertFalse(fc.iterator().hasNext(), "silently fail coercing to point"); + + fc.polygon("layer").setZoomRange(0, 10); + fc.line("layer").setZoomRange(0, 10); + fc.centroid("layer").setZoomRange(0, 10); + fc.pointOnSurface("layer").setZoomRange(0, 10); + var iter = fc.iterator(); + + var item = iter.next(); + assertEquals(GeometryType.POLYGON, item.getGeometryType()); + assertEquals(round(newPolygon( + rectangleCoordList(0, 1), + List.of(rectangleCoordList(0.25, 0.75)) + )), round(item.getGeometry())); + + item = iter.next(); + assertEquals(GeometryType.LINE, item.getGeometryType()); + assertEquals(round(newMultiLineString( + newLineString(0, 0, 1, 0, 1, 1, 0, 1, 0, 0), + newLineString(0.25, 0.25, 0.75, 0.25, 0.75, 0.75, 0.25, 0.75, 0.25, 0.25) + )), round(item.getGeometry())); + + item = iter.next(); + assertEquals(GeometryType.POINT, item.getGeometryType()); + assertEquals(round(newPoint(0.5, 0.5)), round(item.getGeometry())); + + item = iter.next(); + assertEquals(GeometryType.POINT, item.getGeometryType()); + assertEquals(round(newPoint(0.125, 0.5)), round(item.getGeometry())); + + assertFalse(iter.hasNext()); + } + + @Test + public void testPointOnSurface() { + var sourceLine = new ReaderFeature(newPolygon(worldToLatLon( + 0, 0, + 1, 0, + 1, 0.25, + 0.25, 0.25, + 0.25, 0.75, + 1, 0.75, + 1, 1, + 0, 1, + 0, 0 + )), Map.of()); + + var fc = factory.get(sourceLine); + fc.centroid("layer").setZoomRange(0, 10); + fc.pointOnSurface("layer").setZoomRange(0, 10); + var iter = fc.iterator(); + + var item = iter.next(); + assertEquals(GeometryType.POINT, item.getGeometryType()); + assertEquals(round(newPoint(0.425, 0.5)), round(item.getGeometry())); + + item = iter.next(); + assertEquals(GeometryType.POINT, item.getGeometryType()); + assertEquals(round(newPoint(0.125, 0.5)), round(item.getGeometry())); + + assertFalse(iter.hasNext()); + } + + @Test + public void testMultiPolygonCoercion() throws GeometryException { + var sourceLine = new ReaderFeature(newMultiPolygon( + newPolygon(worldToLatLon( + 0, 0, + 1, 0, + 1, 1, + 0, 1, + 0, 0 + )), newPolygon(worldToLatLon( + 2, 0, + 3, 0, + 3, 1, + 2, 1, + 2, 0 + ))), Map.of()); + assertEquals(2, sourceLine.area(), 1e-5); + assertEquals(8, sourceLine.length(), 1e-5); + + var fc = factory.get(sourceLine); + fc.point("layer").setZoomRange(0, 10); + assertFalse(fc.iterator().hasNext(), "silently fail coercing to point"); + + fc.polygon("layer").setZoomRange(0, 10); + fc.line("layer").setZoomRange(0, 10); + fc.centroid("layer").setZoomRange(0, 10); + fc.pointOnSurface("layer").setZoomRange(0, 10); + var iter = fc.iterator(); + + var item = iter.next(); + assertEquals(GeometryType.POLYGON, item.getGeometryType()); + assertEquals(round(newMultiPolygon( + rectangle(0, 1), + rectangle(2, 0, 3, 1) + )), round(item.getGeometry())); + + item = iter.next(); + assertEquals(GeometryType.LINE, item.getGeometryType()); + assertEquals(round(newMultiLineString( + newLineString(rectangleCoordList(0, 1)), + newLineString(rectangleCoordList(2, 0, 3, 1)) + )), round(item.getGeometry())); + + item = iter.next(); + assertEquals(GeometryType.POINT, item.getGeometryType()); + assertEquals(round(newPoint(1.5, 0.5)), round(item.getGeometry())); + + item = iter.next(); + assertEquals(GeometryType.POINT, item.getGeometryType()); + assertEquals(round(newPoint(0.5, 0.5)), round(item.getGeometry())); + + assertFalse(iter.hasNext()); + } + + @Test + public void testMultiLineStringCoercion() throws GeometryException { + var sourceLine = new ReaderFeature(newMultiLineString( + newLineString(worldToLatLon( + 0, 0, + 1, 0, + 1, 1, + 0, 1, + 0, 0 + )), newLineString(worldToLatLon( + 2, 0, + 3, 0, + 3, 1, + 2, 1, + 2, 0 + ))), Map.of()); + assertEquals(0, sourceLine.area(), 1e-5); + assertEquals(8, sourceLine.length(), 1e-5); + + var fc = factory.get(sourceLine); + fc.point("layer").setZoomRange(0, 10); + fc.polygon("layer").setZoomRange(0, 10); + assertFalse(fc.iterator().hasNext(), "silently fail coercing to point/polygon"); + + fc.line("layer").setZoomRange(0, 10); + fc.centroid("layer").setZoomRange(0, 10); + fc.pointOnSurface("layer").setZoomRange(0, 10); + var iter = fc.iterator(); + + var item = iter.next(); + assertEquals(GeometryType.LINE, item.getGeometryType()); + assertEquals(round(newMultiLineString( + newLineString(rectangleCoordList(0, 1)), + newLineString(rectangleCoordList(2, 0, 3, 1)) + )), round(item.getGeometry())); + + item = iter.next(); + assertEquals(GeometryType.POINT, item.getGeometryType()); + assertEquals(round(newPoint(1.5, 0.5)), round(item.getGeometry())); + + item = iter.next(); + assertEquals(GeometryType.POINT, item.getGeometryType()); + assertEquals(round(newPoint(1, 0)), round(item.getGeometry())); + + assertFalse(iter.hasNext()); + } } diff --git a/src/test/java/com/onthegomap/flatmap/TestUtils.java b/src/test/java/com/onthegomap/flatmap/TestUtils.java index 1337dc52..4613669a 100644 --- a/src/test/java/com/onthegomap/flatmap/TestUtils.java +++ b/src/test/java/com/onthegomap/flatmap/TestUtils.java @@ -129,7 +129,11 @@ public class TestUtils { } public static LineString newLineString(double... coords) { - return GeoUtils.JTS_FACTORY.createLineString(newCoordinateList(coords).toArray(new Coordinate[0])); + return newLineString(newCoordinateList(coords)); + } + + public static LineString newLineString(List coords) { + return GeoUtils.JTS_FACTORY.createLineString(coords.toArray(new Coordinate[0])); } public static MultiLineString newMultiLineString(LineString... lineStrings) { diff --git a/src/test/java/com/onthegomap/flatmap/geo/GeoUtilsTest.java b/src/test/java/com/onthegomap/flatmap/geo/GeoUtilsTest.java index 7c87a9b4..8aec68f3 100644 --- a/src/test/java/com/onthegomap/flatmap/geo/GeoUtilsTest.java +++ b/src/test/java/com/onthegomap/flatmap/geo/GeoUtilsTest.java @@ -1,6 +1,12 @@ package com.onthegomap.flatmap.geo; +import static com.onthegomap.flatmap.TestUtils.newLineString; +import static com.onthegomap.flatmap.TestUtils.newMultiLineString; +import static com.onthegomap.flatmap.TestUtils.newMultiPolygon; import static com.onthegomap.flatmap.TestUtils.newPoint; +import static com.onthegomap.flatmap.TestUtils.newPolygon; +import static com.onthegomap.flatmap.TestUtils.rectangle; +import static com.onthegomap.flatmap.TestUtils.rectangleCoordList; import static com.onthegomap.flatmap.TestUtils.round; import static com.onthegomap.flatmap.geo.GeoUtils.ProjectWorldCoords; import static com.onthegomap.flatmap.geo.GeoUtils.decodeWorldX; @@ -10,6 +16,8 @@ import static com.onthegomap.flatmap.geo.GeoUtils.getWorldX; import static com.onthegomap.flatmap.geo.GeoUtils.getWorldY; import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.List; +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; @@ -37,4 +45,67 @@ public class GeoUtilsTest { Geometry actual = ProjectWorldCoords.transform(input); assertEquals(round(expected), round(actual)); } + + @Test + public void testPolygonToLineString() throws GeometryException { + assertEquals(newLineString( + 0, 0, + 1, 0, + 1, 1, + 0, 1, + 0, 0 + ), GeoUtils.polygonToLineString(rectangle( + 0, 1 + ))); + } + + @Test + public void testMultiPolygonToLineString() throws GeometryException { + assertEquals(newLineString( + 0, 0, + 1, 0, + 1, 1, + 0, 1, + 0, 0 + ), GeoUtils.polygonToLineString(newMultiPolygon(rectangle( + 0, 1 + )))); + } + + @Test + public void testLineRingToLineString() throws GeometryException { + assertEquals(newLineString( + 0, 0, + 1, 0, + 1, 1, + 0, 1, + 0, 0 + ), GeoUtils.polygonToLineString(rectangle( + 0, 1 + ).getExteriorRing())); + } + + @Test + public void testComplexPolygonToLineString() throws GeometryException { + assertEquals(newMultiLineString( + newLineString( + 0, 0, + 3, 0, + 3, 3, + 0, 3, + 0, 0 + ), newLineString( + 1, 1, + 2, 1, + 2, 2, + 1, 2, + 1, 1 + ) + ), GeoUtils.polygonToLineString(newPolygon( + rectangleCoordList( + 0, 3 + ), List.of(rectangleCoordList( + 1, 2 + ))))); + } }