From 762c7961d79dd93a7e07c9810475f1ec3f976c34 Mon Sep 17 00:00:00 2001 From: Mike Barry Date: Wed, 15 Nov 2023 06:40:07 -0500 Subject: [PATCH] min zoom for pixel size --- .../planetiler/FeatureCollector.java | 34 ++++++++- .../onthegomap/planetiler/geo/GeoUtils.java | 14 ++++ .../planetiler/PlanetilerTests.java | 70 +++++++++++++++++++ .../planetiler/geo/GeoUtilsTest.java | 26 +++++++ 4 files changed, 143 insertions(+), 1 deletion(-) diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java index 680b393faa..58d824f00f 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java @@ -703,6 +703,27 @@ public Feature setAttrWithMinzoom(String key, Object value, int minzoom) { return setAttr(key, ZoomFunction.minZoom(minzoom, value)); } + /** + * Sets the value for {@code key} only at zoom levels where the feature is at least {@code minPixelSize} pixels in + * size. + */ + public Feature setAttrWithMinSize(String key, Object value, double minPixelSize) { + return setAttrWithMinzoom(key, value, getMinZoomForPixelSize(minPixelSize)); + } + + /** + * Sets the value for {@code key} so that it always shows when {@code zoom_level >= minZoomToShowAlways} but only + * shows when {@code minZoomIfBigEnough <= zoom_level < minZoomToShowAlways} when it is at least + * {@code minPixelSize} pixels in size. + *

+ * If you need more flexibility, use {@link #getMinZoomForPixelSize(double)} directly. + */ + public Feature setAttrWithMinSize(String key, Object value, double minPixelSize, int minZoomIfBigEnough, + int minZoomToShowAlways) { + return setAttrWithMinzoom(key, value, + Math.clamp(getMinZoomForPixelSize(minPixelSize), minZoomIfBigEnough, minZoomToShowAlways)); + } + /** * Inserts all key/value pairs in {@code attrs} into the set of attribute to emit on the output feature at or above * {@code minzoom}. @@ -766,9 +787,20 @@ public double getSourceFeaturePixelSizeAtZoom(int zoom) { try { return source.size() * (256 << zoom); } catch (GeometryException e) { - e.log(stats, "point_get_size_failure", "Error getting min size for point from geometry " + source.id()); + e.log(stats, "source_feature_pixel_size_at_zoom_failure", + "Error getting source feature pixel size at zoom from geometry " + source.id()); return 0; } } + + /** Returns the minimum zoom level at which this feature is at least {@code pixelSize} pixels large. */ + public int getMinZoomForPixelSize(double pixelSize) { + try { + return GeoUtils.minZoomForPixelSize(source.size(), pixelSize); + } catch (GeometryException e) { + e.log(stats, "min_zoom_for_size_failure", "Error getting min zoom for size from geometry " + source.id()); + return config.maxzoom(); + } + } } } diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeoUtils.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeoUtils.java index 0809f4de4b..f6d0aceb7b 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeoUtils.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/GeoUtils.java @@ -1,6 +1,7 @@ package com.onthegomap.planetiler.geo; import com.onthegomap.planetiler.collection.LongLongMap; +import com.onthegomap.planetiler.config.PlanetilerConfig; import com.onthegomap.planetiler.stats.Stats; import java.util.ArrayList; import java.util.List; @@ -51,6 +52,7 @@ public class GeoUtils { public static final double WORLD_CIRCUMFERENCE_METERS = Math.PI * 2 * WORLD_RADIUS_METERS; private static final double RADIANS_PER_DEGREE = Math.PI / 180; private static final double DEGREES_PER_RADIAN = 180 / Math.PI; + private static final double LOG2 = Math.log(2); /** * Transform web mercator coordinates where top-left corner of the planet is (0,0) and bottom-right is (1,1) to * latitude/longitude coordinates. @@ -534,6 +536,18 @@ public static Geometry combine(Geometry... geometries) { JTS_FACTORY.createGeometryCollection(innerGeometries.toArray(Geometry[]::new)); } + /** + * For a feature of size {@code worldGeometrySize} (where 1=full planet), determine the minimum zoom level at which + * the feature appears at least {@code minPixelSize} pixels large. + *

+ * The result will be clamped to the range [0, {@link PlanetilerConfig#MAX_MAXZOOM}]. + */ + public static int minZoomForPixelSize(double worldGeometrySize, double minPixelSize) { + double worldPixels = worldGeometrySize * 256; + return Math.clamp((int) Math.ceil(Math.log(minPixelSize / worldPixels) / LOG2), 0, + PlanetilerConfig.MAX_MAXZOOM); + } + /** Helper class to sort polygons by area of their outer shell. */ private record PolyAndArea(Polygon poly, double area) implements Comparable { diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java index f44ada3556..d1224780f3 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java @@ -87,6 +87,8 @@ class PlanetilerTests { private static final double Z13_WIDTH = 1d / Z13_TILES; private static final int Z12_TILES = 1 << 12; private static final double Z12_WIDTH = 1d / Z12_TILES; + private static final int Z11_TILES = 1 << 11; + private static final double Z11_WIDTH = 1d / Z11_TILES; private static final int Z4_TILES = 1 << 4; private static final Polygon WORLD_POLYGON = newPolygon( worldCoordinateList( @@ -2434,6 +2436,74 @@ void testCentroidWithLineMinSize() throws Exception { ), results.tiles); } + @Test + void testAttributeMinSizeLine() throws Exception { + List points = z14CoordinatePixelList(0, 4, 40, 4); + + var results = runWithReaderFeatures( + Map.of("threads", "1"), + List.of( + newReaderFeature(newLineString(points), Map.of()) + ), + (in, features) -> features.line("layer") + .setZoomRange(11, 14) + .setBufferPixels(0) + .setAttrWithMinSize("a", "1", 10) + .setAttrWithMinSize("b", "2", 20) + .setAttrWithMinSize("c", "3", 40) + .setAttrWithMinSize("d", "4", 40, 0, 13) // should show up at z13 and above + ); + + assertEquals(Map.ofEntries( + newTileEntry(Z11_TILES / 2, Z11_TILES / 2, 11, List.of( + feature(newLineString(0, 0.5, 5, 0.5), Map.of()) + )), + newTileEntry(Z12_TILES / 2, Z12_TILES / 2, 12, List.of( + feature(newLineString(0, 1, 10, 1), Map.of("a", "1")) + )), + newTileEntry(Z13_TILES / 2, Z13_TILES / 2, 13, List.of( + feature(newLineString(0, 2, 20, 2), Map.of("a", "1", "b", "2", "d", "4")) + )), + newTileEntry(Z14_TILES / 2, Z14_TILES / 2, 14, List.of( + feature(newLineString(0, 4, 40, 4), Map.of("a", "1", "b", "2", "c", "3", "d", "4")) + )) + ), results.tiles); + } + + @Test + void testAttributeMinSizePoint() throws Exception { + List points = z14CoordinatePixelList(0, 4, 40, 4); + + var results = runWithReaderFeatures( + Map.of("threads", "1"), + List.of( + newReaderFeature(newLineString(points), Map.of()) + ), + (in, features) -> features.centroid("layer") + .setZoomRange(11, 14) + .setBufferPixels(0) + .setAttrWithMinSize("a", "1", 10) + .setAttrWithMinSize("b", "2", 20) + .setAttrWithMinSize("c", "3", 40) + .setAttrWithMinSize("d", "4", 40, 0, 13) // should show up at z13 and above + ); + + assertEquals(Map.ofEntries( + newTileEntry(Z11_TILES / 2, Z11_TILES / 2, 11, List.of( + feature(newPoint(2.5, 0.5), Map.of()) + )), + newTileEntry(Z12_TILES / 2, Z12_TILES / 2, 12, List.of( + feature(newPoint(5, 1), Map.of("a", "1")) + )), + newTileEntry(Z13_TILES / 2, Z13_TILES / 2, 13, List.of( + feature(newPoint(10, 2), Map.of("a", "1", "b", "2", "d", "4")) + )), + newTileEntry(Z14_TILES / 2, Z14_TILES / 2, 14, List.of( + feature(newPoint(20, 4), Map.of("a", "1", "b", "2", "c", "3", "d", "4")) + )) + ), results.tiles); + } + @Test void testBoundFiltersFill() throws Exception { var polyResultz8 = runForBoundsTest(8, 8, "polygon", TestUtils.pathToResource("bottomrightearth.poly").toString()); diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/GeoUtilsTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/GeoUtilsTest.java index f1b528f9ac..dead314178 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/GeoUtilsTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/geo/GeoUtilsTest.java @@ -447,4 +447,30 @@ void testSnapAndFixIssue546_3() throws GeometryException, ParseException { assertTrue(result.isValid()); assertFalse(result.contains(point)); } + + @ParameterizedTest + @CsvSource({ + "1,0,0", + "1,10,0", + "1,255,0", + + "0.5,0,0", + "0.5,128,0", + "0.5,129,1", + "0.5,256,1", + + "0.25,0,0", + "0.25,128,1", + "0.25,129,2", + "0.25,256,2", + }) + void minZoomForPixelSize(double worldGeometrySize, double minPixelSize, int expectedMinZoom) { + assertEquals(expectedMinZoom, GeoUtils.minZoomForPixelSize(worldGeometrySize, minPixelSize)); + } + + @Test + void minZoomForPixelSizesAtZ9_10() { + assertEquals(10, GeoUtils.minZoomForPixelSize(3.1 / (256 << 10), 3)); + assertEquals(9, GeoUtils.minZoomForPixelSize(6.1 / (256 << 10), 3)); + } }