Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add setAttrWithMinSize to feature API #725

Merged
merged 3 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,28 @@ public Feature innermostPoint(String layer) {
return innermostPoint(layer, 0.1);
}

/** 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();
}
}


/** Returns the actual pixel size of the source feature at {@code zoom} (length if line, sqrt(area) if polygon). */
public double getPixelSizeAtZoom(int zoom) {
try {
return source.size() * (256 << zoom);
} catch (GeometryException e) {
e.log(stats, "source_feature_pixel_size_at_zoom_failure",
"Error getting source feature pixel size at zoom from geometry " + source.id());
return 0;
}
}

/**
* Creates new feature collector instances for each source feature that we encounter.
*/
Expand Down Expand Up @@ -703,6 +725,29 @@ 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.
* <p>
* If you need more flexibility, use {@link #getMinZoomForPixelSize(double)} directly, or create a
* {@link ZoomFunction} that calculates {@link #getPixelSizeAtZoom(int)} and applies a custom threshold based on the
* zoom level.
*/
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}.
Expand Down Expand Up @@ -736,20 +781,20 @@ public Feature putAttrs(Map<String, Object> attrs) {
}

/**
* Sets a special attribute key that the renderer will use to store the number of points in the simplified geometry
* Returns the attribute key that the renderer should use to store the number of points in the simplified geometry
* before slicing it into tiles.
*/
public Feature setNumPointsAttr(String numPointsAttr) {
this.numPointsAttr = numPointsAttr;
return this;
public String getNumPointsAttr() {
return numPointsAttr;
}

/**
* Returns the attribute key that the renderer should use to store the number of points in the simplified geometry
* Sets a special attribute key that the renderer will use to store the number of points in the simplified geometry
* before slicing it into tiles.
*/
public String getNumPointsAttr() {
return numPointsAttr;
public Feature setNumPointsAttr(String numPointsAttr) {
this.numPointsAttr = numPointsAttr;
return this;
}

@Override
Expand All @@ -763,12 +808,7 @@ public String toString() {

/** Returns the actual pixel size of the source feature at {@code zoom} (length if line, sqrt(area) if polygon). */
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());
return 0;
}
return getPixelSizeAtZoom(zoom);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
* <p>
* 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<PolyAndArea> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -2434,6 +2436,74 @@ void testCentroidWithLineMinSize() throws Exception {
), results.tiles);
}

@Test
void testAttributeMinSizeLine() throws Exception {
List<Coordinate> 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<Coordinate> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Loading