From 1e36e9e940f43f61c6cf38b07e88df71ee72b123 Mon Sep 17 00:00:00 2001 From: Mike Barry Date: Sun, 16 May 2021 06:42:57 -0400 Subject: [PATCH] render points --- .../onthegomap/flatmap/FeatureCollector.java | 38 +-- .../onthegomap/flatmap/FeatureRenderer.java | 122 +++++---- .../com/onthegomap/flatmap/geo/GeoUtils.java | 19 +- .../com/onthegomap/flatmap/geo/TileCoord.java | 5 +- .../flatmap/read/OpenStreetMapReader.java | 5 +- .../flatmap/FeatureCollectorTest.java | 4 +- .../flatmap/FeatureRendererTest.java | 239 ++++++++++++++++++ .../{FeatureTest.java => FlatMapTest.java} | 47 +++- .../onthegomap/flatmap/LayerStatsTest.java | 3 +- .../com/onthegomap/flatmap/TestUtils.java | 94 ++++++- 10 files changed, 496 insertions(+), 80 deletions(-) create mode 100644 src/test/java/com/onthegomap/flatmap/FeatureRendererTest.java rename src/test/java/com/onthegomap/flatmap/{FeatureTest.java => FlatMapTest.java} (82%) diff --git a/src/main/java/com/onthegomap/flatmap/FeatureCollector.java b/src/main/java/com/onthegomap/flatmap/FeatureCollector.java index 47652f22..ee3daaac 100644 --- a/src/main/java/com/onthegomap/flatmap/FeatureCollector.java +++ b/src/main/java/com/onthegomap/flatmap/FeatureCollector.java @@ -5,14 +5,12 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; -import java.util.concurrent.atomic.AtomicLong; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.Lineal; import org.locationtech.jts.geom.Puntal; public class FeatureCollector implements Iterable> { - private static final AtomicLong idGen = new AtomicLong(0); private final SourceFeature source; private final List> output = new ArrayList<>(); private final CommonParams config; @@ -36,7 +34,7 @@ public class FeatureCollector implements Iterable> { } public PointFeature point(String layer) { - var feature = new PointFeature(layer, source.centroid()); + var feature = new PointFeature(layer, source.isPoint() ? source.worldGeometry() : source.centroid()); output.add(feature); return feature; } @@ -97,6 +95,10 @@ public class FeatureCollector implements Iterable> { protected PointFeature self() { return this; } + + public boolean hasLabelGrid() { + return labelGridPixelSize != null || labelGridLimit != null; + } } public class LineFeature extends Feature { @@ -162,18 +164,16 @@ public class FeatureCollector implements Iterable> { private final String layer; private final Geometry geom; - private final long id; private int zOrder; private int minzoom = config.minzoom(); private int maxzoom = config.maxzoom(); - private double defaultBuffer; - private ZoomFunction bufferOverrides; + private double defaultBufferPixels = 4; + private ZoomFunction bufferPixelOverrides; private final Map attrs = new TreeMap<>(); private Feature(String layer, Geometry geom) { this.layer = layer; this.geom = geom; - this.id = idGen.incrementAndGet(); this.zOrder = 0; } @@ -210,10 +210,6 @@ public class FeatureCollector implements Iterable> { return maxzoom; } - public long getId() { - return id; - } - public String getLayer() { return layer; } @@ -222,17 +218,17 @@ public class FeatureCollector implements Iterable> { return geom; } - public double getBufferAtZoom(int zoom) { - return ZoomFunction.applyAsDoubleOrElse(bufferOverrides, zoom, defaultBuffer); + public double getBufferPixelsAtZoom(int zoom) { + return ZoomFunction.applyAsDoubleOrElse(bufferPixelOverrides, zoom, defaultBufferPixels); } - public T setBuffer(double buffer) { - defaultBuffer = buffer; + public T setBufferPixels(double buffer) { + defaultBufferPixels = buffer; return self(); } - public T setBufferOverrides(ZoomFunction buffer) { - bufferOverrides = buffer; + public T setBufferPixelOverrides(ZoomFunction buffer) { + bufferPixelOverrides = buffer; return self(); } @@ -264,5 +260,13 @@ public class FeatureCollector implements Iterable> { return self(); } + @Override + public String toString() { + return "Feature{" + + "layer='" + layer + '\'' + + ", geom=" + geom.getGeometryType() + + ", attrs=" + attrs + + '}'; + } } } diff --git a/src/main/java/com/onthegomap/flatmap/FeatureRenderer.java b/src/main/java/com/onthegomap/flatmap/FeatureRenderer.java index c56fbcfe..9dfd435d 100644 --- a/src/main/java/com/onthegomap/flatmap/FeatureRenderer.java +++ b/src/main/java/com/onthegomap/flatmap/FeatureRenderer.java @@ -3,10 +3,15 @@ package com.onthegomap.flatmap; import com.onthegomap.flatmap.geo.GeoUtils; import com.onthegomap.flatmap.geo.TileCoord; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateXY; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryCollection; import org.locationtech.jts.geom.LineString; @@ -20,6 +25,8 @@ import org.slf4j.LoggerFactory; public class FeatureRenderer { + private static final AtomicLong idGen = new AtomicLong(0); + private static final Logger LOGGER = LoggerFactory.getLogger(FeatureRenderer.class); private final CommonParams config; private final Consumer consumer; @@ -33,12 +40,14 @@ public class FeatureRenderer { renderGeometry(feature.getGeometry(), feature); } - public void renderGeometry(Geometry geom, FeatureCollector.Feature feature) { + private void renderGeometry(Geometry geom, FeatureCollector.Feature feature) { // TODO what about converting between area and line? // TODO generate ID in here? - if (feature instanceof FeatureCollector.PointFeature pointFeature) { + if (geom.isEmpty()) { + LOGGER.warn("Empty geometry " + feature); + } else if (feature instanceof FeatureCollector.PointFeature pointFeature) { if (geom instanceof Point point) { - addPointFeature(pointFeature, point); + addPointFeature(pointFeature, point.getCoordinates()); } else if (geom instanceof MultiPoint points) { addPointFeature(pointFeature, points); } else { @@ -82,7 +91,7 @@ public class FeatureRenderer { // } } - private static double clip(double value, double max) { + private static int wrapInt(int value, int max) { value %= max; if (value < 0) { value += max; @@ -90,55 +99,80 @@ public class FeatureRenderer { return value; } - private void addPointFeature(FeatureCollector.PointFeature feature, Point point) { + private static double wrapDouble(double value, double max) { + value %= max; + if (value < 0) { + value += max; + } + return value; + } + + private void slicePoint(Map> output, int zoom, double buffer, Coordinate coord) { + int tilesAtZoom = 1 << zoom; + double worldX = coord.getX() * tilesAtZoom; + double worldY = coord.getY() * tilesAtZoom; + int minX = (int) Math.floor(worldX - buffer); + int maxX = (int) Math.floor(worldX + buffer); + int minY = Math.max(0, (int) Math.floor(worldY - buffer)); + int maxY = Math.min(tilesAtZoom - 1, (int) Math.floor(worldY + buffer)); + for (int x = minX; x <= maxX; x++) { + double tileX = worldX - x; + for (int y = minY; y <= maxY; y++) { + TileCoord tile = TileCoord.ofXYZ(wrapInt(x, tilesAtZoom), y, zoom); + double tileY = worldY - y; + Coordinate outCoordinate = new CoordinateXY(tileX * 256, tileY * 256); + output.computeIfAbsent(tile, t -> new HashSet<>()).add(outCoordinate); + } + } + } + + private void addPointFeature(FeatureCollector.PointFeature feature, Coordinate... coords) { + long id = idGen.incrementAndGet(); for (int zoom = feature.getMaxZoom(); zoom >= feature.getMinZoom(); zoom--) { + Map> sliced = new HashMap<>(); Map attrs = feature.getAttrsAtZoom(zoom); - double buffer = feature.getBufferAtZoom(zoom); - double tilesAtZoom = 1 << zoom; - double worldX = point.getX() * tilesAtZoom; - double worldY = point.getY() * tilesAtZoom; + double buffer = feature.getBufferPixelsAtZoom(zoom) / 256; + int tilesAtZoom = 1 << zoom; + for (Coordinate coord : coords) { + slicePoint(sliced, zoom, buffer, coord); + } - double labelGridTileSize = feature.getLabelGridPixelSizeAtZoom(zoom) / 256d; - Optional groupInfo = labelGridTileSize >= 1d / 4096d ? - Optional.of(new RenderedFeature.Group(GeoUtils.longPair( - (int) Math.floor(clip(worldX, tilesAtZoom) / labelGridTileSize), - (int) Math.floor(worldY / labelGridTileSize) - ), feature.getLabelGridLimitAtZoom(zoom))) : Optional.empty(); + Optional groupInfo = Optional.empty(); + if (feature.hasLabelGrid() && coords.length == 1) { + double labelGridTileSize = feature.getLabelGridPixelSizeAtZoom(zoom) / 256d; + groupInfo = labelGridTileSize >= 1d / 4096d ? + Optional.of(new RenderedFeature.Group(GeoUtils.longPair( + (int) Math.floor(wrapDouble(coords[0].getX() * tilesAtZoom, tilesAtZoom) / labelGridTileSize), + (int) Math.floor((coords[0].getY() * tilesAtZoom) / labelGridTileSize) + ), feature.getLabelGridLimitAtZoom(zoom))) : Optional.empty(); + } - int minX = (int) (worldX - buffer); - int maxX = (int) (worldX + buffer); - int minY = (int) (worldY - buffer); - int maxY = (int) (worldY + buffer); - - // TODO test (real, feature, unit) - // TODO wrap x at z0 - // TODO multipoints? - // TODO factor-out rendered feature creation - for (int x = minX; x <= maxX; x++) { - for (int y = minY; y <= maxY; y++) { - TileCoord tile = TileCoord.ofXYZ(x, y, zoom); - double tileX = worldX - x; - double tileY = worldY - y; - consumer.accept(new RenderedFeature( - tile, - new VectorTileEncoder.Feature( - feature.getLayer(), - feature.getId(), - VectorTileEncoder.encodeGeometry(GeoUtils.point(tileX * 256, tileY * 256)), - attrs - ), - feature.getZorder(), - groupInfo - )); - } + for (var entry : sliced.entrySet()) { + Set value = entry.getValue(); + Geometry geom = value.size() == 1 ? GeoUtils.point(value.iterator().next()) : GeoUtils.multiPoint(value); + consumer.accept(new RenderedFeature( + entry.getKey(), + new VectorTileEncoder.Feature( + feature.getLayer(), + id, + VectorTileEncoder.encodeGeometry(geom), + attrs + ), + feature.getZorder(), + groupInfo + )); } } } private void addPointFeature(FeatureCollector.PointFeature feature, MultiPoint points) { - Map> tilePoints = new HashMap<>(); - - // TODO render features into tile + if (feature.hasLabelGrid()) { + for (Coordinate coord : points.getCoordinates()) { + addPointFeature(feature, coord); + } + } else { + addPointFeature(feature, points.getCoordinates()); + } } private void addLinearFeature(FeatureCollector.Feature feature, Geometry geom) { diff --git a/src/main/java/com/onthegomap/flatmap/geo/GeoUtils.java b/src/main/java/com/onthegomap/flatmap/geo/GeoUtils.java index 960627e3..0deb9241 100644 --- a/src/main/java/com/onthegomap/flatmap/geo/GeoUtils.java +++ b/src/main/java/com/onthegomap/flatmap/geo/GeoUtils.java @@ -1,10 +1,14 @@ package com.onthegomap.flatmap.geo; +import java.util.Collection; +import org.locationtech.jts.geom.Coordinate; 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.GeometryFactory; +import org.locationtech.jts.geom.MultiPoint; +import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.impl.PackedCoordinateSequence; import org.locationtech.jts.geom.util.GeometryTransformer; import org.locationtech.jts.io.WKBReader; @@ -169,7 +173,20 @@ public class GeoUtils { return (pair << 16) >> 16; } - public static Geometry point(double x, double y) { + public static Point point(double x, double y) { return gf.createPoint(new CoordinateXY(x, y)); } + + public static Point point(Coordinate coord) { + return gf.createPoint(coord); + } + + public static MultiPoint multiPoint(Collection coords) { + return gf.createMultiPointFromCoords(coords.toArray(new Coordinate[0])); + } + + public static Geometry multiPoint(double... coords) { + assert coords.length % 2 == 0; + return gf.createMultiPoint(new PackedCoordinateSequence.Double(coords, 2, 0)); + } } diff --git a/src/main/java/com/onthegomap/flatmap/geo/TileCoord.java b/src/main/java/com/onthegomap/flatmap/geo/TileCoord.java index 49816a70..3dc1871b 100644 --- a/src/main/java/com/onthegomap/flatmap/geo/TileCoord.java +++ b/src/main/java/com/onthegomap/flatmap/geo/TileCoord.java @@ -67,10 +67,7 @@ public record TileCoord(int encoded, int x, int y, int z) implements Comparable< @Override public String toString() { - return "[" + - z + "/" + x + "/" + y + - " (" + encoded + - ")]"; + return "{x=" + x + " y=" + y + " z=" + z + '}'; } @Override diff --git a/src/main/java/com/onthegomap/flatmap/read/OpenStreetMapReader.java b/src/main/java/com/onthegomap/flatmap/read/OpenStreetMapReader.java index 49e4a82e..d2c60f06 100644 --- a/src/main/java/com/onthegomap/flatmap/read/OpenStreetMapReader.java +++ b/src/main/java/com/onthegomap/flatmap/read/OpenStreetMapReader.java @@ -30,7 +30,6 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicLong; import org.locationtech.jts.geom.CoordinateSequence; -import org.locationtech.jts.geom.CoordinateXY; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.impl.PackedCoordinateSequence; @@ -245,10 +244,10 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima @Override protected Geometry computeWorldGeometry() { - return GeoUtils.gf.createPoint(new CoordinateXY( + return GeoUtils.point( GeoUtils.getWorldX(lon), GeoUtils.getWorldY(lat) - )); + ); } @Override diff --git a/src/test/java/com/onthegomap/flatmap/FeatureCollectorTest.java b/src/test/java/com/onthegomap/flatmap/FeatureCollectorTest.java index fa79e6e0..3c874119 100644 --- a/src/test/java/com/onthegomap/flatmap/FeatureCollectorTest.java +++ b/src/test/java/com/onthegomap/flatmap/FeatureCollectorTest.java @@ -42,8 +42,8 @@ public class FeatureCollectorTest { .setZoomRange(12, 14) .setZorder(3) .setAttr("attr1", 2) - .setBuffer(10d) - .setBufferOverrides(ZoomFunction.maxZoom(12, 11d)) + .setBufferPixels(10d) + .setBufferPixelOverrides(ZoomFunction.maxZoom(12, 11d)) .setLabelGridSizeAndLimit(12, 100, 10); assertFeatures(14, List.of( Map.of( diff --git a/src/test/java/com/onthegomap/flatmap/FeatureRendererTest.java b/src/test/java/com/onthegomap/flatmap/FeatureRendererTest.java new file mode 100644 index 00000000..81a4f532 --- /dev/null +++ b/src/test/java/com/onthegomap/flatmap/FeatureRendererTest.java @@ -0,0 +1,239 @@ +package com.onthegomap.flatmap; + +import static com.onthegomap.flatmap.TestUtils.assertSameNormalizedFeatures; +import static com.onthegomap.flatmap.TestUtils.emptyGeometry; +import static com.onthegomap.flatmap.TestUtils.newMultiPoint; +import static com.onthegomap.flatmap.TestUtils.newPoint; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +import com.onthegomap.flatmap.geo.GeoUtils; +import com.onthegomap.flatmap.geo.TileCoord; +import com.onthegomap.flatmap.read.ReaderFeature; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.locationtech.jts.geom.Geometry; + +public class FeatureRendererTest { + + private final CommonParams config = CommonParams.defaults(); + + private FeatureCollector collector(Geometry worldGeom) { + var latLonGeom = GeoUtils.worldToLatLonCoords(worldGeom); + return new FeatureCollector.Factory(config).get(new ReaderFeature(latLonGeom, 0)); + } + + private Map> renderGeometry(FeatureCollector.Feature feature) { + Map> result = new TreeMap<>(); + new FeatureRenderer(config, rendered -> result.computeIfAbsent(rendered.tile(), tile -> new HashSet<>()) + .add(rendered.vectorTileFeature().geometry().decode())).renderFeature(feature); + return result; + } + + private Map> renderFeatures(FeatureCollector.Feature feature) { + Map> result = new TreeMap<>(); + new FeatureRenderer(config, rendered -> result.computeIfAbsent(rendered.tile(), tile -> new HashSet<>()) + .add(rendered)).renderFeature(feature); + return result; + } + + private static final int Z14_TILES = 1 << 14; + private static final double Z14_WIDTH = 1d / Z14_TILES; + + @Test + public void testEmptyGeometry() { + var feature = collector(emptyGeometry()).point("layer"); + assertSameNormalizedFeatures(Map.of(), renderGeometry(feature)); + } + + @Test + public void testSinglePoint() { + var feature = pointFeature(newPoint(0.5 + Z14_WIDTH / 2, 0.5 + Z14_WIDTH / 2)) + .setZoomRange(14, 14); + assertSameNormalizedFeatures(Map.of( + TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of( + newPoint(128, 128) + ) + ), renderGeometry(feature)); + } + + @Test + public void testRepeatSinglePointNeighboringTiles() { + var feature = pointFeature(newPoint(0.5 + 1d / 512, 0.5 + 1d / 512)) + .setZoomRange(0, 1) + .setBufferPixels(2); + assertSameNormalizedFeatures(Map.of( + TileCoord.ofXYZ(0, 0, 0), List.of(newPoint(128.5, 128.5)), + TileCoord.ofXYZ(0, 0, 1), List.of(newPoint(257, 257)), + TileCoord.ofXYZ(1, 0, 1), List.of(newPoint(1, 257)), + TileCoord.ofXYZ(0, 1, 1), List.of(newPoint(257, 1)), + TileCoord.ofXYZ(1, 1, 1), List.of(newPoint(1, 1)) + ), renderGeometry(feature)); + } + + @TestFactory + public List testProcessPointsNearInternationalDateLineAndPoles() { + double d = 1d / 512; + record X(double x, double wrapped, double z1x0, double z1x1) { + + } + record Y(double y, int z1ty, double tyoff) { + + } + var xs = List.of( + new X(-d, 1 - d, -1, 255), + new X(d, 1 + d, 1, 257), + new X(1 - d, -d, -1, 255), + new X(1 + d, d, 1, 257) + ); + var ys = List.of( + new Y(0.25, 0, 128), + new Y(-d, 0, -1), + new Y(d, 0, 1), + new Y(1 - d, 1, 255), + new Y(1 + d, 1, 257) + ); + List tests = new ArrayList<>(); + for (X x : xs) { + for (Y y : ys) { + tests.add(dynamicTest((x.x * 256) + ", " + (y.y * 256), () -> { + var feature = pointFeature(newPoint(x.x, y.y)) + .setZoomRange(0, 1) + .setBufferPixels(2); + assertSameNormalizedFeatures(Map.of( + TileCoord.ofXYZ(0, 0, 0), List.of(newMultiPoint( + newPoint(x.x * 256, y.y * 256), + newPoint(x.wrapped * 256, y.y * 256) + )), + TileCoord.ofXYZ(0, y.z1ty, 1), List.of(newPoint(x.z1x0, y.tyoff)), + TileCoord.ofXYZ(1, y.z1ty, 1), List.of(newPoint(x.z1x1, y.tyoff)) + ), renderGeometry(feature)); + })); + } + } + + return tests; + } + + @Test + public void testZ0FullTileBuffer() { + var feature = pointFeature(newPoint(0.25, 0.25)) + .setZoomRange(0, 1) + .setBufferPixels(256); + assertSameNormalizedFeatures(Map.of( + TileCoord.ofXYZ(0, 0, 0), List.of(newMultiPoint( + newPoint(-192, 64), + newPoint(64, 64), + newPoint(320, 64) + )), + TileCoord.ofXYZ(0, 0, 1), List.of(newPoint(128, 128)), + TileCoord.ofXYZ(1, 0, 1), List.of(newMultiPoint( + newPoint(-128, 128), + newPoint(256 + 128, 128) + )), + TileCoord.ofXYZ(0, 1, 1), List.of(newPoint(128, -128)), + TileCoord.ofXYZ(1, 1, 1), List.of(newMultiPoint( + newPoint(-128, -128), + newPoint(256 + 128, -128) + )) + ), renderGeometry(feature)); + } + + @Test + public void testMultipointNoLabelGrid() { + var feature = pointFeature(newMultiPoint( + newPoint(0.25, 0.25), + newPoint(0.25 + 1d / 256, 0.25 + 1d / 256) + )) + .setZoomRange(0, 1) + .setBufferPixels(4); + assertSameNormalizedFeatures(Map.of( + TileCoord.ofXYZ(0, 0, 0), List.of(newMultiPoint( + newPoint(64, 64), + newPoint(65, 65) + )), + TileCoord.ofXYZ(0, 0, 1), List.of(newMultiPoint( + newPoint(128, 128), + newPoint(130, 130) + )) + ), renderGeometry(feature)); + } + + @Test + public void testMultipointWithLabelGridSplits() { + var feature = pointFeature(newMultiPoint( + newPoint(0.25, 0.25), + newPoint(0.25 + 1d / 256, 0.25 + 1d / 256) + )) + .setLabelGridPixelSize(10, 256) + .setZoomRange(0, 1) + .setBufferPixels(4); + assertSameNormalizedFeatures(Map.of( + TileCoord.ofXYZ(0, 0, 0), List.of( + newPoint(64, 64), + newPoint(65, 65) + ), + TileCoord.ofXYZ(0, 0, 1), List.of( + newPoint(128, 128), + newPoint(130, 130) + ) + ), renderGeometry(feature)); + } + + @Test + public void testLabelGrid() { + var feature = pointFeature(newPoint(0.75, 0.75)) + .setLabelGridSizeAndLimit(10, 256, 2) + .setZoomRange(0, 1) + .setBufferPixels(4); + var rendered = renderFeatures(feature); + var z0Feature = rendered.get(TileCoord.ofXYZ(0, 0, 0)).iterator().next(); + var z1Feature = rendered.get(TileCoord.ofXYZ(1, 1, 1)).iterator().next(); + assertEquals(Optional.of(new RenderedFeature.Group(0, 2)), z0Feature.group()); + assertEquals(Optional.of(new RenderedFeature.Group((1L << 32) + 1, 2)), z1Feature.group()); + } + + @Test + public void testWrapLabelGrid() { + var feature = pointFeature(newPoint(1.1, -0.1)) + .setLabelGridSizeAndLimit(10, 256, 2) + .setZoomRange(0, 1) + .setBufferPixels(64); + var rendered = renderFeatures(feature); + System.err.println(rendered); + var z0Feature = rendered.get(TileCoord.ofXYZ(0, 0, 0)).iterator().next(); + var z1Feature = rendered.get(TileCoord.ofXYZ(0, 0, 1)).iterator().next(); + assertEquals(Optional.of(new RenderedFeature.Group((1L << 32) - 1, 2)), z0Feature.group()); + assertEquals(Optional.of(new RenderedFeature.Group((1L << 32) - 1, 2)), z1Feature.group()); + } + + private FeatureCollector.PointFeature pointFeature(Geometry geom) { + return collector(geom).point("layer"); + } + + // happy tests: + // TODO: multipoint together? + // TODO: multipoint separate (if has group)? separate IDs? + // TODO: world wrap + + // TODO: line + // TODO: multilinestring + // TODO: poly + // TODO: multipolygon + // TODO: geometry collection + + // sad tests: + // TODO: invalid line + // TODO: invalid poly + // TODO: coerce poly -> line + // TODO: coerce line -> poly + // TODO: wrong types: point/line/poly -> point/line/poly +} diff --git a/src/test/java/com/onthegomap/flatmap/FeatureTest.java b/src/test/java/com/onthegomap/flatmap/FlatMapTest.java similarity index 82% rename from src/test/java/com/onthegomap/flatmap/FeatureTest.java rename to src/test/java/com/onthegomap/flatmap/FlatMapTest.java index 8bf303a2..1edc70c4 100644 --- a/src/test/java/com/onthegomap/flatmap/FeatureTest.java +++ b/src/test/java/com/onthegomap/flatmap/FlatMapTest.java @@ -28,7 +28,10 @@ import java.util.function.BiConsumer; import java.util.function.Function; import org.junit.jupiter.api.Test; -public class FeatureTest { +/** + * In-memory tests with fake data and profiles to ensure all features work end-to-end. + */ +public class FlatMapTest { private static final String TEST_PROFILE_NAME = "test name"; private static final String TEST_PROFILE_DESCRIPTION = "test description"; @@ -162,6 +165,46 @@ public class FeatureTest { ); } + @Test + public void testLabelGridLimit() throws IOException, SQLException { + double y = 0.5 + Z14_WIDTH / 2; + double lat = GeoUtils.getWorldLat(y); + + double x1 = 0.5 + Z14_WIDTH / 4; + double lng1 = GeoUtils.getWorldLon(x1); + double lng2 = GeoUtils.getWorldLon(x1 + Z14_WIDTH * 10d / 256); + double lng3 = GeoUtils.getWorldLon(x1 + Z14_WIDTH * 20d / 256); + + var results = runWithReaderFeatures( + Map.of("threads", "1"), + List.of( + new ReaderFeature(newPoint(lng1, lat), Map.of("rank", "1")), + new ReaderFeature(newPoint(lng2, lat), Map.of("rank", "2")), + new ReaderFeature(newPoint(lng3, lat), Map.of("rank", "3")) + ), + (in, features) -> { + features.point("layer") + .setZoomRange(13, 14) + .inheritFromSource("rank") + .setZorder(Integer.parseInt(in.getTag("rank").toString())) + .setLabelGridSizeAndLimit(13, 128, 2); + } + ); + + assertSubmap(Map.of( + TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of( + feature(newPoint(64, 128), Map.of("rank", "1")), + feature(newPoint(74, 128), Map.of("rank", "2")), + feature(newPoint(84, 128), Map.of("rank", "3")) + ), + TileCoord.ofXYZ(Z13_TILES / 2, Z13_TILES / 2, 13), List.of( + // omit rank=1 due to label grid size + feature(newPoint(37, 64), Map.of("rank", "2")), + feature(newPoint(42, 64), Map.of("rank", "3")) + ) + ), results.tiles); + } + private interface LayerPostprocessFunction { List process(String layer, int zoom, List items); @@ -218,6 +261,4 @@ public class FeatureTest { return postprocessLayerFeatures.process(layer, zoom, items); } } - - // TODO: refactor into parameterized test? } diff --git a/src/test/java/com/onthegomap/flatmap/LayerStatsTest.java b/src/test/java/com/onthegomap/flatmap/LayerStatsTest.java index 0401e78d..90cddb18 100644 --- a/src/test/java/com/onthegomap/flatmap/LayerStatsTest.java +++ b/src/test/java/com/onthegomap/flatmap/LayerStatsTest.java @@ -86,7 +86,6 @@ public class LayerStatsTest { ), layerStats.getTileStats()); } - @Test public void testMergeFromMultipleThreads() throws InterruptedException { Thread t1 = new Thread(() -> { @@ -103,7 +102,6 @@ public class LayerStatsTest { )); }); t1.start(); - t1.join(); Thread t2 = new Thread(() -> { layerStats.accept(new RenderedFeature( TileCoord.ofXYZ(1, 2, 4), @@ -118,6 +116,7 @@ public class LayerStatsTest { )); }); t2.start(); + t1.join(); t2.join(); assertEquals(new Mbtiles.MetadataJson( new Mbtiles.MetadataJson.VectorLayer("layer1", Map.of( diff --git a/src/test/java/com/onthegomap/flatmap/TestUtils.java b/src/test/java/com/onthegomap/flatmap/TestUtils.java index e33cc1e0..361b5177 100644 --- a/src/test/java/com/onthegomap/flatmap/TestUtils.java +++ b/src/test/java/com/onthegomap/flatmap/TestUtils.java @@ -13,11 +13,15 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.function.Function; +import java.util.stream.Collectors; import java.util.zip.GZIPInputStream; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateSequence; @@ -148,25 +152,67 @@ public class TestUtils { assertEquals(new TreeMap<>(expectedSubmap), actualFiltered, message + " others: " + others); } + public static Geometry emptyGeometry() { + return GeoUtils.gf.createGeometryCollection(); + } + public interface GeometryComparision { Geometry geom(); } - public static record ExactGeometry(Geometry geom) implements GeometryComparision { + private static record NormGeometry(Geometry geom) implements GeometryComparision { + + @Override + public boolean equals(Object o) { + return o instanceof GeometryComparision that && geom.equalsNorm(that.geom()); + } + + @Override + public String toString() { + return "Norm{" + geom + '}'; + } + + @Override + public int hashCode() { + return 0; + } + } + + private static record ExactGeometry(Geometry geom) implements GeometryComparision { @Override public boolean equals(Object o) { return o instanceof GeometryComparision that && geom.equalsExact(that.geom()); } + + @Override + public String toString() { + return "Exact{" + geom + '}'; + } + + @Override + public int hashCode() { + return 0; + } } - public static record TopoGeometry(Geometry geom) implements GeometryComparision { + private static record TopoGeometry(Geometry geom) implements GeometryComparision { @Override public boolean equals(Object o) { return o instanceof GeometryComparision that && geom.equalsTopo(that.geom()); } + + @Override + public String toString() { + return "Topo{" + geom + '}'; + } + + @Override + public int hashCode() { + return 0; + } } public static record ComparableFeature( @@ -184,8 +230,7 @@ public class TestUtils { TreeMap result = new TreeMap<>(feature.getAttrsAtZoom(zoom)); result.put("_minzoom", feature.getMinZoom()); result.put("_maxzoom", feature.getMaxZoom()); - result.put("_buffer", feature.getBufferAtZoom(zoom)); - result.put("_id", feature.getId()); + result.put("_buffer", feature.getBufferPixelsAtZoom(zoom)); result.put("_layer", feature.getLayer()); result.put("_zorder", feature.getZorder()); result.put("_geom", new TopoGeometry(feature.getGeometry())); @@ -211,4 +256,45 @@ public class TestUtils { objectMapper.readTree(actual) ); } + + public static Map> mapTileFeatures(Map> in, + Function fn) { + TreeMap> out = new TreeMap<>(); + for (var entry : in.entrySet()) { + out.put(entry.getKey(), entry.getValue().stream().map(fn) + .sorted(Comparator.comparing(Object::toString)) + .collect(Collectors.toList())); + } + return out; + } + + public static void assertExactSameFeatures( + Map> expected, + Map> actual + ) { + assertEquals( + mapTileFeatures(expected, ExactGeometry::new), + mapTileFeatures(actual, ExactGeometry::new) + ); + } + + public static void assertTopologicallyEquivalentFeatures( + Map> expected, + Map> actual + ) { + assertEquals( + mapTileFeatures(expected, TopoGeometry::new), + mapTileFeatures(actual, TopoGeometry::new) + ); + } + + public static void assertSameNormalizedFeatures( + Map> expected, + Map> actual + ) { + assertEquals( + mapTileFeatures(expected, NormGeometry::new), + mapTileFeatures(actual, NormGeometry::new) + ); + } }