From 7e70005cc3258d834bcbb9cf325662507d4659db Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Fri, 26 Dec 2025 11:48:52 -0800 Subject: [PATCH 01/33] Added base case POI zoom in new MultiExpression --- .../com/protomaps/basemap/layers/Pois.java | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index 40f981ae0..f6f7c565b 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -2,6 +2,7 @@ import static com.onthegomap.planetiler.util.Parse.parseDoubleOrNull; import static com.protomaps.basemap.feature.Matcher.fromTag; +import static com.protomaps.basemap.feature.Matcher.getInteger; import static com.protomaps.basemap.feature.Matcher.getString; import static com.protomaps.basemap.feature.Matcher.rule; import static com.protomaps.basemap.feature.Matcher.use; @@ -47,7 +48,7 @@ public Pois(QrankDb qrankDb) { "US Forest Service", "U.S. Forest Service", "USDA Forest Service", "United States Department of Agriculture", "US National Forest Service", "United State Forest Service", "U.S. National Forest Service"); - private static final MultiExpression.Index> index = MultiExpression.of(List.of( + private static final MultiExpression.Index> kindsIndex = MultiExpression.of(List.of( // Everything is "other"/"" at first rule(use("kind", "other"), use("kindDetail", "")), @@ -144,6 +145,13 @@ public Pois(QrankDb qrankDb) { )).index(); + private static final MultiExpression.Index> zoomsIndex = MultiExpression.of(List.of( + + // Everything is zoom=15 at first + rule(use("minZoom", 15)) + + )).index(); + @Override public String name() { return LAYER_NAME; @@ -154,13 +162,20 @@ public String name() { Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d, 2); public void processOsm(SourceFeature sf, FeatureCollector features) { - var matches = index.getMatches(sf); - if (matches.isEmpty()) { + var kindMatches = kindsIndex.getMatches(sf); + if (kindMatches.isEmpty()) { + return; + } + + String kind = getString(sf, kindMatches, "kind", "undefined"); + String kindDetail = getString(sf, kindMatches, "kindDetail", "undefined"); + + var zoomMatches = zoomsIndex.getMatches(sf); + if (zoomMatches.isEmpty()) { return; } - String kind = getString(sf, matches, "kind", "undefined"); - String kindDetail = getString(sf, matches, "kindDetail", "undefined"); + Integer minZoom = getInteger(sf, zoomMatches, "minZoom", 99); if ((sf.isPoint() || sf.canBePolygon()) && (sf.hasTag("aeroway", "aerodrome") || sf.hasTag("amenity") || @@ -177,7 +192,6 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { sf.hasTag("shop") || sf.hasTag("tourism") && (!sf.hasTag("historic", "district")))) { - Integer minZoom = 15; long qrank = 0; String wikidata = sf.getString("wikidata"); From 3a62861dd7819cfa8f03e8d5f593b27dc6cf9889 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Fri, 26 Dec 2025 12:40:56 -0800 Subject: [PATCH 02/33] Moved initial zoom assignments to MultiExpression, seeing one unexplained failure with aerodrome/iata tags --- .../com/protomaps/basemap/layers/Pois.java | 112 ++++++++---------- 1 file changed, 51 insertions(+), 61 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index f6f7c565b..dd8b05a62 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -148,7 +148,30 @@ public Pois(QrankDb qrankDb) { private static final MultiExpression.Index> zoomsIndex = MultiExpression.of(List.of( // Everything is zoom=15 at first - rule(use("minZoom", 15)) + rule(use("minZoom", 15)), + + rule(with("protomaps-basemaps:kind", "national_park"), use("minZoom", 11)), + rule(with("natural", "peak"), use("minZoom", 13)), + rule(with("highway", "bus_stop"), use("minZoom", 17)), + rule(with("tourism", "attraction", "camp_site", "hotel"), use("minZoom", 15)), + rule(with("shop", "grocery", "supermarket"), use("minZoom", 14)), + rule(with("leisure", "golf_course", "marina", "stadium"), use("minZoom", 13)), + rule(with("leisure", "park"), use("minZoom", 14)), // Lots of pocket parks and NODE parks, show those later than rest of leisure + rule(with("landuse", "cemetery"), use("minZoom", 14)), + rule(with("amenity", "cafe"), use("minZoom", 15)), + rule(with("amenity", "school"), use("minZoom", 15)), + rule(with("amenity", "library", "post_office", "townhall"), use("minZoom", 13)), + rule(with("amenity", "hospital"), use("minZoom", 12)), + rule(with("amenity", "university", "college"), use("minZoom", 14)), // One would think University should be earlier, but there are lots of dinky node only places, so if the university has a large area, it'll naturally improve its zoom in another section... + rule(with("aeroway", "aerodrome"), use("minZoom", 13)), + + // Emphasize large international airports earlier + rule( + with("aeroway", "aerodrome"), + with("protomaps-basemaps:kind", "aerodrome"), + with("iata"), + use("minZoom", 11) + ) )).index(); @@ -157,24 +180,47 @@ public String name() { return LAYER_NAME; } - // ~= pow((sqrt(70k) / (40m / 256)) / 256, 2) ~= 4.4e-11 + // ~= pow((sqrt(7e4) / (4e7 / 256)) / 256, 2) ~= 4.4e-11 private static final double WORLD_AREA_FOR_70K_SQUARE_METERS = Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d, 2); + public void calculateDimensions(SourceFeature sf) { + Double wayArea = 0.0; + Double height = 0.0; + + if (sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null) { + try { + wayArea = sf.worldGeometry().getEnvelopeInternal().getArea() / WORLD_AREA_FOR_70K_SQUARE_METERS; + } catch (GeometryException e) { + e.log("Exception in POI way calculation"); + } + if (sf.hasTag("height")) { + Double parsed = parseDoubleOrNull(sf.getString("height")); + if (parsed != null) { + height = parsed; + } + } + } + + sf.setTag("protomaps-basemaps:wayArea", wayArea); + sf.setTag("protomaps-basemaps:height", height); + } + public void processOsm(SourceFeature sf, FeatureCollector features) { var kindMatches = kindsIndex.getMatches(sf); if (kindMatches.isEmpty()) { return; } - String kind = getString(sf, kindMatches, "kind", "undefined"); - String kindDetail = getString(sf, kindMatches, "kindDetail", "undefined"); - + calculateDimensions(sf); + sf.setTag("protomaps-basemaps:kind", getString(sf, kindMatches, "kind", "undefined")); var zoomMatches = zoomsIndex.getMatches(sf); if (zoomMatches.isEmpty()) { return; } + String kind = getString(sf, kindMatches, "kind", "undefined"); + String kindDetail = getString(sf, kindMatches, "kindDetail", "undefined"); Integer minZoom = getInteger(sf, zoomMatches, "minZoom", 99); if ((sf.isPoint() || sf.canBePolygon()) && (sf.hasTag("aeroway", "aerodrome") || @@ -199,62 +245,6 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { qrank = qrankDb.get(wikidata); } - if (sf.hasTag("aeroway", "aerodrome")) { - minZoom = 13; - - // Emphasize large international airports earlier - if (kind.equals("aerodrome") && sf.hasTag("iata")) { - minZoom -= 2; - } - } else if (sf.hasTag("amenity", "university", "college")) { - // One would think University should be earlier, but there are lots of dinky node only places - // So if the university has a large area, it'll naturally improve it's zoom in the next section... - minZoom = 14; - } else if (sf.hasTag("amenity", "hospital")) { - minZoom = 12; - } else if (sf.hasTag("amenity", "library", "post_office", "townhall")) { - minZoom = 13; - } else if (sf.hasTag("amenity", "school")) { - minZoom = 15; - } else if (sf.hasTag("amenity", "cafe")) { - minZoom = 15; - } else if (sf.hasTag("landuse", "cemetery")) { - minZoom = 14; - } else if (sf.hasTag("leisure", "park")) { - // Lots of pocket parks and NODE parks, show those later than rest of leisure - minZoom = 14; - } else if (sf.hasTag("leisure", "golf_course", "marina", "stadium")) { - minZoom = 13; - } else if (sf.hasTag("shop", "grocery", "supermarket")) { - minZoom = 14; - } else if (sf.hasTag("tourism", "attraction", "camp_site", "hotel")) { - minZoom = 15; - } else if (sf.hasTag("highway", "bus_stop")) { - minZoom = 17; - } else if (sf.hasTag("natural", "peak")) { - minZoom = 13; - } - - // National parks - if (sf.hasTag("boundary", "national_park")) { - if (!(sf.hasTag("operator", "United States Forest Service", "US Forest Service", "U.S. Forest Service", - "USDA Forest Service", "United States Department of Agriculture", "US National Forest Service", - "United State Forest Service", "U.S. National Forest Service") || - sf.hasTag("protection_title", "Conservation Area", "Conservation Park", "Environmental use", "Forest Reserve", - "National Forest", "National Wildlife Refuge", "Nature Refuge", "Nature Reserve", "Protected Site", - "Provincial Park", "Public Access Land", "Regional Reserve", "Resources Reserve", "State Forest", - "State Game Land", "State Park", "Watershed Recreation Unit", "Wild Forest", "Wilderness Area", - "Wilderness Study Area", "Wildlife Management", "Wildlife Management Area", "Wildlife Sanctuary")) && - (sf.hasTag("protect_class", "2", "3") || - sf.hasTag("operator", "United States National Park Service", "National Park Service", - "US National Park Service", "U.S. National Park Service", "US National Park service") || - sf.hasTag("operator:en", "Parks Canada") || - sf.hasTag("designation", "national_park") || - sf.hasTag("protection_title", "National Park"))) { - minZoom = 11; - } - } - // try first for polygon -> point representations if (sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null) { Double wayArea = 0.0; From 03316e2a08f3a3e792a804f3c023e9778cb697c1 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Fri, 26 Dec 2025 12:55:05 -0800 Subject: [PATCH 03/33] Moved a bunch of high-zoom point logic to MultiExpression --- .../com/protomaps/basemap/layers/Pois.java | 75 +++++++++++-------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index dd8b05a62..955f65c22 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -7,6 +7,7 @@ import static com.protomaps.basemap.feature.Matcher.rule; import static com.protomaps.basemap.feature.Matcher.use; import static com.protomaps.basemap.feature.Matcher.with; +import static com.protomaps.basemap.feature.Matcher.withPoint; import static com.protomaps.basemap.feature.Matcher.without; import com.onthegomap.planetiler.FeatureCollector; @@ -171,6 +172,46 @@ public Pois(QrankDb qrankDb) { with("protomaps-basemaps:kind", "aerodrome"), with("iata"), use("minZoom", 11) + ), + + rule( + withPoint(), + Expression.or( + with("amenity", "clinic", "dentist", "doctors", "social_facility", "baby_hatch", "childcare", + "car_sharing", "bureau_de_change", "emergency_phone", "karaoke", "karaoke_box", "money_transfer", "car_wash", + "hunting_stand", "studio", "boat_storage", "gambling", "adult_gaming_centre", "sanitary_dump_station", + "attraction", "animal", "water_slide", "roller_coaster", "summer_toboggan", "carousel", "amusement_ride", + "maze"), + with("historic", "memorial", "district"), + with("leisure", "pitch", "playground", "slipway"), + with("shop", "scuba_diving", "atv", "motorcycle", "snowmobile", "art", "bakery", "beauty", "bookmaker", + "books", "butcher", "car", "car_parts", "car_repair", "clothes", "computer", "convenience", "fashion", + "florist", "garden_centre", "gift", "golf", "greengrocer", "grocery", "hairdresser", "hifi", "jewelry", + "lottery", "mobile_phone", "newsagent", "optician", "perfumery", "ship_chandler", "stationery", "tobacco", + "travel_agency"), + with("tourism", "artwork", "hanami", "trail_riding_station", "bed_and_breakfast", "chalet", + "guest_house", "hostel") + ), + use("minZoom", 16) + ), + + // Some features should only be visible at very late zooms when they don't have a name + rule( + withPoint(), + without("name"), + Expression.or( + with("amenity", "atm", "bbq", "bench", "bicycle_parking", + "bicycle_rental", "bicycle_repair_station", "boat_storage", "bureau_de_change", "car_rental", "car_sharing", + "car_wash", "charging_station", "customs", "drinking_water", "fuel", "harbourmaster", "hunting_stand", + "karaoke_box", "life_ring", "money_transfer", "motorcycle_parking", "parking", "picnic_table", "post_box", + "ranger_station", "recycling", "sanitary_dump_station", "shelter", "shower", "taxi", "telephone", "toilets", + "waste_basket", "waste_disposal", "water_point", "watering_place", "bicycle_rental", "motorcycle_parking", + "charging_station"), + with("historic", "landmark", "wayside_cross"), + with("leisure", "dog_park", "firepit", "fishing", "pitch", "playground", "slipway", "swimming_area"), + with("tourism", "alpine_hut", "information", "picnic_site", "viewpoint", "wilderness_hut") + ), + use("minZoom", 16) ) )).index(); @@ -187,8 +228,10 @@ public String name() { public void calculateDimensions(SourceFeature sf) { Double wayArea = 0.0; Double height = 0.0; + String namedPolygon = "no"; if (sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null) { + namedPolygon = "yes"; try { wayArea = sf.worldGeometry().getEnvelopeInternal().getArea() / WORLD_AREA_FOR_70K_SQUARE_METERS; } catch (GeometryException e) { @@ -204,6 +247,7 @@ public void calculateDimensions(SourceFeature sf) { sf.setTag("protomaps-basemaps:wayArea", wayArea); sf.setTag("protomaps-basemaps:height", height); + sf.setTag("protomaps-basemaps:namedPolygon", namedPolygon); } public void processOsm(SourceFeature sf, FeatureCollector features) { @@ -538,37 +582,6 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { OsmNames.setOsmNames(pointFeature, sf, 0); - // Some features should only be visible at very late zooms when they don't have a name - if (!sf.hasTag("name") && (sf.hasTag("amenity", "atm", "bbq", "bench", "bicycle_parking", - "bicycle_rental", "bicycle_repair_station", "boat_storage", "bureau_de_change", "car_rental", "car_sharing", - "car_wash", "charging_station", "customs", "drinking_water", "fuel", "harbourmaster", "hunting_stand", - "karaoke_box", "life_ring", "money_transfer", "motorcycle_parking", "parking", "picnic_table", "post_box", - "ranger_station", "recycling", "sanitary_dump_station", "shelter", "shower", "taxi", "telephone", "toilets", - "waste_basket", "waste_disposal", "water_point", "watering_place", "bicycle_rental", "motorcycle_parking", - "charging_station") || - sf.hasTag("historic", "landmark", "wayside_cross") || - sf.hasTag("leisure", "dog_park", "firepit", "fishing", "pitch", "playground", "slipway", "swimming_area") || - sf.hasTag("tourism", "alpine_hut", "information", "picnic_site", "viewpoint", "wilderness_hut"))) { - pointFeature.setAttr("min_zoom", 17); - } - - if (sf.hasTag("amenity", "clinic", "dentist", "doctors", "social_facility", "baby_hatch", "childcare", - "car_sharing", "bureau_de_change", "emergency_phone", "karaoke", "karaoke_box", "money_transfer", "car_wash", - "hunting_stand", "studio", "boat_storage", "gambling", "adult_gaming_centre", "sanitary_dump_station", - "attraction", "animal", "water_slide", "roller_coaster", "summer_toboggan", "carousel", "amusement_ride", - "maze") || - sf.hasTag("historic", "memorial", "district") || - sf.hasTag("leisure", "pitch", "playground", "slipway") || - sf.hasTag("shop", "scuba_diving", "atv", "motorcycle", "snowmobile", "art", "bakery", "beauty", "bookmaker", - "books", "butcher", "car", "car_parts", "car_repair", "clothes", "computer", "convenience", "fashion", - "florist", "garden_centre", "gift", "golf", "greengrocer", "grocery", "hairdresser", "hifi", "jewelry", - "lottery", "mobile_phone", "newsagent", "optician", "perfumery", "ship_chandler", "stationery", "tobacco", - "travel_agency") || - sf.hasTag("tourism", "artwork", "hanami", "trail_riding_station", "bed_and_breakfast", "chalet", - "guest_house", "hostel")) { - pointFeature.setAttr("min_zoom", 17); - } - // Server sort features so client label collisions are pre-sorted // NOTE: (nvkelso 20230627) This could also include other params like the name pointFeature.setSortKey(minZoom * 1000); From 88218734a351b92ce95c7e4668ea71b7aff92b05 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Fri, 26 Dec 2025 13:24:11 -0800 Subject: [PATCH 04/33] Created Matcher.SourceFeatureWithComputedTags() to allow mutation of tags for zoom checks --- .../protomaps/basemap/feature/Matcher.java | 81 +++++++++++++++++-- .../com/protomaps/basemap/layers/Pois.java | 32 +++++--- 2 files changed, 93 insertions(+), 20 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java b/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java index 24291d414..2c3280371 100644 --- a/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java +++ b/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java @@ -2,13 +2,17 @@ import com.onthegomap.planetiler.expression.Expression; import com.onthegomap.planetiler.expression.MultiExpression; +import com.onthegomap.planetiler.geo.GeometryException; import com.onthegomap.planetiler.geo.GeometryType; +import com.onthegomap.planetiler.geo.WithGeometry; import com.onthegomap.planetiler.reader.SourceFeature; +import com.onthegomap.planetiler.reader.WithTags; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.locationtech.jts.geom.Geometry; /** * A utility class for matching source feature properties to values. @@ -203,14 +207,14 @@ public static FromTag fromTag(String key) { return new FromTag(key); } - public static String getString(SourceFeature sf, List> matches, String key, String defaultValue) { + public static String getString(WithTags wt, List> matches, String key, String defaultValue) { for (var match : matches.reversed()) { if (match.containsKey(key)) { Object value = match.get(key); if (value instanceof String stringValue) { return stringValue; } else if (value instanceof FromTag fromTag) { - return sf.getString(fromTag.key, defaultValue); + return wt.getString(fromTag.key, defaultValue); } else { return defaultValue; } @@ -219,7 +223,7 @@ public static String getString(SourceFeature sf, List> match return defaultValue; } - public static Integer getInteger(SourceFeature sf, List> matches, String key, + public static Integer getInteger(WithTags wt, List> matches, String key, Integer defaultValue) { for (var match : matches.reversed()) { if (match.containsKey(key)) { @@ -228,7 +232,7 @@ public static Integer getInteger(SourceFeature sf, List> mat return integerValue; } else if (value instanceof FromTag fromTag) { try { - return sf.hasTag(fromTag.key) ? Integer.valueOf(sf.getString(fromTag.key)) : defaultValue; + return wt.hasTag(fromTag.key) ? Integer.valueOf(wt.getString(fromTag.key)) : defaultValue; } catch (NumberFormatException e) { return defaultValue; } @@ -240,7 +244,7 @@ public static Integer getInteger(SourceFeature sf, List> mat return defaultValue; } - public static Double getDouble(SourceFeature sf, List> matches, String key, Double defaultValue) { + public static Double getDouble(WithTags wt, List> matches, String key, Double defaultValue) { for (var match : matches.reversed()) { if (match.containsKey(key)) { Object value = match.get(key); @@ -248,7 +252,7 @@ public static Double getDouble(SourceFeature sf, List> match return doubleValue; } else if (value instanceof FromTag fromTag) { try { - return sf.hasTag(fromTag.key) ? Double.valueOf(sf.getString(fromTag.key)) : defaultValue; + return wt.hasTag(fromTag.key) ? Double.valueOf(wt.getString(fromTag.key)) : defaultValue; } catch (NumberFormatException e) { return defaultValue; } @@ -260,7 +264,7 @@ public static Double getDouble(SourceFeature sf, List> match return defaultValue; } - public static Boolean getBoolean(SourceFeature sf, List> matches, String key, + public static Boolean getBoolean(WithTags wt, List> matches, String key, Boolean defaultValue) { for (var match : matches.reversed()) { if (match.containsKey(key)) { @@ -268,7 +272,7 @@ public static Boolean getBoolean(SourceFeature sf, List> mat if (value instanceof Boolean booleanValue) { return booleanValue; } else if (value instanceof FromTag fromTag) { - return sf.hasTag(fromTag.key) ? sf.getBoolean(fromTag.key) : defaultValue; + return wt.hasTag(fromTag.key) ? wt.getBoolean(fromTag.key) : defaultValue; } else { return defaultValue; } @@ -277,4 +281,65 @@ public static Boolean getBoolean(SourceFeature sf, List> mat return defaultValue; } + /** + * Wrapper that combines a SourceFeature with computed tags without mutating the original. This allows MultiExpression + * matching to access both original and computed tags. + * + *

+ * This is useful when you need to add computed tags (like area calculations or derived properties) that should be + * accessible to MultiExpression rules, but the original SourceFeature has immutable tags. + *

+ */ + public static class SourceFeatureWithComputedTags extends WithGeometry implements WithTags { + private final SourceFeature delegate; + private final Map combinedTags; + + /** + * Creates a wrapper around a SourceFeature with additional computed tags. + * + * @param delegate The original SourceFeature to wrap + * @param computedTags Additional computed tags to merge with the original tags + */ + public SourceFeatureWithComputedTags(SourceFeature delegate, Map computedTags) { + this.delegate = delegate; + this.combinedTags = new HashMap<>(delegate.tags()); + this.combinedTags.putAll(computedTags); + } + + @Override + public Map tags() { + return combinedTags; + } + + @Override + public Geometry worldGeometry() throws GeometryException { + return delegate.worldGeometry(); + } + + @Override + public Geometry latLonGeometry() throws GeometryException { + return delegate.latLonGeometry(); + } + + @Override + public boolean isPoint() { + return delegate.isPoint(); + } + + @Override + public boolean canBePolygon() { + return delegate.canBePolygon(); + } + + @Override + public boolean canBeLine() { + return delegate.canBeLine(); + } + + /** Returns the original SourceFeature being wrapped */ + public SourceFeature getDelegate() { + return delegate; + } + } + } diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index 955f65c22..74b437c4a 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -19,8 +19,10 @@ import com.onthegomap.planetiler.geo.GeometryException; import com.onthegomap.planetiler.reader.SourceFeature; import com.protomaps.basemap.feature.FeatureId; +import com.protomaps.basemap.feature.Matcher; import com.protomaps.basemap.feature.QrankDb; import com.protomaps.basemap.names.OsmNames; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -225,13 +227,13 @@ public String name() { private static final double WORLD_AREA_FOR_70K_SQUARE_METERS = Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d, 2); - public void calculateDimensions(SourceFeature sf) { + public Map calculateDimensions(SourceFeature sf) { Double wayArea = 0.0; Double height = 0.0; - String namedPolygon = "no"; + Boolean hasNamedPolygon = false; if (sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null) { - namedPolygon = "yes"; + hasNamedPolygon = true; try { wayArea = sf.worldGeometry().getEnvelopeInternal().getArea() / WORLD_AREA_FOR_70K_SQUARE_METERS; } catch (GeometryException e) { @@ -245,9 +247,11 @@ public void calculateDimensions(SourceFeature sf) { } } - sf.setTag("protomaps-basemaps:wayArea", wayArea); - sf.setTag("protomaps-basemaps:height", height); - sf.setTag("protomaps-basemaps:namedPolygon", namedPolygon); + return Map.of( + "protomaps-basemaps:wayArea", wayArea, + "protomaps-basemaps:height", height, + "protomaps-basemaps:hasNamedPolygon", hasNamedPolygon + ); } public void processOsm(SourceFeature sf, FeatureCollector features) { @@ -256,16 +260,20 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { return; } - calculateDimensions(sf); - sf.setTag("protomaps-basemaps:kind", getString(sf, kindMatches, "kind", "undefined")); - var zoomMatches = zoomsIndex.getMatches(sf); + String kind = getString(sf, kindMatches, "kind", "undefined"); + String kindDetail = getString(sf, kindMatches, "kindDetail", "undefined"); + + // Calculate dimensions and create a wrapper with computed tags + Map computedTags = new HashMap<>(calculateDimensions(sf)); + computedTags.put("protomaps-basemaps:kind", kind); + + var sf2 = new Matcher.SourceFeatureWithComputedTags(sf, computedTags); + var zoomMatches = zoomsIndex.getMatches(sf2); if (zoomMatches.isEmpty()) { return; } - String kind = getString(sf, kindMatches, "kind", "undefined"); - String kindDetail = getString(sf, kindMatches, "kindDetail", "undefined"); - Integer minZoom = getInteger(sf, zoomMatches, "minZoom", 99); + Integer minZoom = getInteger(sf2, zoomMatches, "minZoom", 99); if ((sf.isPoint() || sf.canBePolygon()) && (sf.hasTag("aeroway", "aerodrome") || sf.hasTag("amenity") || From f7a49281325128093ff2445800e61048ba943773 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Fri, 26 Dec 2025 13:36:52 -0800 Subject: [PATCH 05/33] Fixed Matcher.SourceFeatureWithComputedTags() to require fewer signature changes --- .../protomaps/basemap/feature/Matcher.java | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java b/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java index 2c3280371..748ed8ace 100644 --- a/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java +++ b/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java @@ -7,6 +7,8 @@ import com.onthegomap.planetiler.geo.WithGeometry; import com.onthegomap.planetiler.reader.SourceFeature; import com.onthegomap.planetiler.reader.WithTags; +import com.onthegomap.planetiler.reader.osm.OsmReader; +import com.onthegomap.planetiler.reader.osm.OsmRelationInfo; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -207,14 +209,14 @@ public static FromTag fromTag(String key) { return new FromTag(key); } - public static String getString(WithTags wt, List> matches, String key, String defaultValue) { + public static String getString(SourceFeature sf, List> matches, String key, String defaultValue) { for (var match : matches.reversed()) { if (match.containsKey(key)) { Object value = match.get(key); if (value instanceof String stringValue) { return stringValue; } else if (value instanceof FromTag fromTag) { - return wt.getString(fromTag.key, defaultValue); + return sf.getString(fromTag.key, defaultValue); } else { return defaultValue; } @@ -223,7 +225,7 @@ public static String getString(WithTags wt, List> matches, S return defaultValue; } - public static Integer getInteger(WithTags wt, List> matches, String key, + public static Integer getInteger(SourceFeature sf, List> matches, String key, Integer defaultValue) { for (var match : matches.reversed()) { if (match.containsKey(key)) { @@ -232,7 +234,7 @@ public static Integer getInteger(WithTags wt, List> matches, return integerValue; } else if (value instanceof FromTag fromTag) { try { - return wt.hasTag(fromTag.key) ? Integer.valueOf(wt.getString(fromTag.key)) : defaultValue; + return sf.hasTag(fromTag.key) ? Integer.valueOf(sf.getString(fromTag.key)) : defaultValue; } catch (NumberFormatException e) { return defaultValue; } @@ -244,7 +246,7 @@ public static Integer getInteger(WithTags wt, List> matches, return defaultValue; } - public static Double getDouble(WithTags wt, List> matches, String key, Double defaultValue) { + public static Double getDouble(SourceFeature sf, List> matches, String key, Double defaultValue) { for (var match : matches.reversed()) { if (match.containsKey(key)) { Object value = match.get(key); @@ -252,7 +254,7 @@ public static Double getDouble(WithTags wt, List> matches, S return doubleValue; } else if (value instanceof FromTag fromTag) { try { - return wt.hasTag(fromTag.key) ? Double.valueOf(wt.getString(fromTag.key)) : defaultValue; + return sf.hasTag(fromTag.key) ? Double.valueOf(sf.getString(fromTag.key)) : defaultValue; } catch (NumberFormatException e) { return defaultValue; } @@ -264,7 +266,7 @@ public static Double getDouble(WithTags wt, List> matches, S return defaultValue; } - public static Boolean getBoolean(WithTags wt, List> matches, String key, + public static Boolean getBoolean(SourceFeature sf, List> matches, String key, Boolean defaultValue) { for (var match : matches.reversed()) { if (match.containsKey(key)) { @@ -272,7 +274,7 @@ public static Boolean getBoolean(WithTags wt, List> matches, if (value instanceof Boolean booleanValue) { return booleanValue; } else if (value instanceof FromTag fromTag) { - return wt.hasTag(fromTag.key) ? wt.getBoolean(fromTag.key) : defaultValue; + return sf.hasTag(fromTag.key) ? sf.getBoolean(fromTag.key) : defaultValue; } else { return defaultValue; } @@ -290,7 +292,7 @@ public static Boolean getBoolean(WithTags wt, List> matches, * accessible to MultiExpression rules, but the original SourceFeature has immutable tags. *

*/ - public static class SourceFeatureWithComputedTags extends WithGeometry implements WithTags { + public static class SourceFeatureWithComputedTags extends SourceFeature { private final SourceFeature delegate; private final Map combinedTags; @@ -301,6 +303,7 @@ public static class SourceFeatureWithComputedTags extends WithGeometry implement * @param computedTags Additional computed tags to merge with the original tags */ public SourceFeatureWithComputedTags(SourceFeature delegate, Map computedTags) { + super(new HashMap<>(delegate.tags()), delegate.getSource(), delegate.getSourceLayer(), null, delegate.id()); this.delegate = delegate; this.combinedTags = new HashMap<>(delegate.tags()); this.combinedTags.putAll(computedTags); @@ -336,9 +339,9 @@ public boolean canBeLine() { return delegate.canBeLine(); } - /** Returns the original SourceFeature being wrapped */ - public SourceFeature getDelegate() { - return delegate; + @Override + public boolean hasRelationInfo() { + return delegate.hasRelationInfo(); } } From 5c529a5383519f9af3ef821fca62b1c53885b4b3 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Fri, 26 Dec 2025 13:46:03 -0800 Subject: [PATCH 06/33] Moved SourceFeatureWithComputedTags construction into computeExtraTags --- .../protomaps/basemap/feature/Matcher.java | 4 --- .../com/protomaps/basemap/layers/Pois.java | 25 +++++++++---------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java b/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java index 748ed8ace..446250ba0 100644 --- a/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java +++ b/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java @@ -4,11 +4,7 @@ import com.onthegomap.planetiler.expression.MultiExpression; import com.onthegomap.planetiler.geo.GeometryException; import com.onthegomap.planetiler.geo.GeometryType; -import com.onthegomap.planetiler.geo.WithGeometry; import com.onthegomap.planetiler.reader.SourceFeature; -import com.onthegomap.planetiler.reader.WithTags; -import com.onthegomap.planetiler.reader.osm.OsmReader; -import com.onthegomap.planetiler.reader.osm.OsmRelationInfo; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index 74b437c4a..69f1008c1 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -22,7 +22,6 @@ import com.protomaps.basemap.feature.Matcher; import com.protomaps.basemap.feature.QrankDb; import com.protomaps.basemap.names.OsmNames; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -227,7 +226,7 @@ public String name() { private static final double WORLD_AREA_FOR_70K_SQUARE_METERS = Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d, 2); - public Map calculateDimensions(SourceFeature sf) { + public Matcher.SourceFeatureWithComputedTags computeExtraTags(SourceFeature sf, String kind) { Double wayArea = 0.0; Double height = 0.0; Boolean hasNamedPolygon = false; @@ -247,10 +246,14 @@ public Map calculateDimensions(SourceFeature sf) { } } - return Map.of( - "protomaps-basemaps:wayArea", wayArea, - "protomaps-basemaps:height", height, - "protomaps-basemaps:hasNamedPolygon", hasNamedPolygon + return new Matcher.SourceFeatureWithComputedTags( + sf, + Map.of( + "protomaps-basemaps:kind", kind, + "protomaps-basemaps:wayArea", wayArea, + "protomaps-basemaps:height", height, + "protomaps-basemaps:hasNamedPolygon", hasNamedPolygon + ) ); } @@ -260,19 +263,15 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { return; } - String kind = getString(sf, kindMatches, "kind", "undefined"); - String kindDetail = getString(sf, kindMatches, "kindDetail", "undefined"); - // Calculate dimensions and create a wrapper with computed tags - Map computedTags = new HashMap<>(calculateDimensions(sf)); - computedTags.put("protomaps-basemaps:kind", kind); - - var sf2 = new Matcher.SourceFeatureWithComputedTags(sf, computedTags); + var sf2 = computeExtraTags(sf, getString(sf, kindMatches, "kind", "undefined")); var zoomMatches = zoomsIndex.getMatches(sf2); if (zoomMatches.isEmpty()) { return; } + String kind = getString(sf2, kindMatches, "kind", "undefined"); + String kindDetail = getString(sf2, kindMatches, "kindDetail", "undefined"); Integer minZoom = getInteger(sf2, zoomMatches, "minZoom", 99); if ((sf.isPoint() || sf.canBePolygon()) && (sf.hasTag("aeroway", "aerodrome") || From 5711a0e6601fda62af80f58a798f0f7cd426e834 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Fri, 26 Dec 2025 14:07:32 -0800 Subject: [PATCH 07/33] Moved selected small-area polygons into rules --- .../com/protomaps/basemap/layers/Pois.java | 85 ++++++++++++++----- 1 file changed, 63 insertions(+), 22 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index 69f1008c1..95b63fa49 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -22,6 +22,7 @@ import com.protomaps.basemap.feature.Matcher; import com.protomaps.basemap.feature.QrankDb; import com.protomaps.basemap.names.OsmNames; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -213,6 +214,51 @@ public Pois(QrankDb qrankDb) { with("tourism", "alpine_hut", "information", "picnic_site", "viewpoint", "wilderness_hut") ), use("minZoom", 16) + ), + + rule( + with("protomaps-basemaps:hasNamedPolygon"), + with("protomaps-basemaps:kind", "playground"), + use("minZoom", 17) + ), + rule( + with("protomaps-basemaps:hasNamedPolygon"), + with("protomaps-basemaps:kind", "allotments"), + use("minZoom", 16) + ), + rule( + with("protomaps-basemaps:hasNamedPolygon"), + Expression.or(with("protomaps-basemaps:kind", "cemetery"), with("protomaps-basemaps:kind", "school")), + use("minZoom", 16) + ), + rule( + with("protomaps-basemaps:hasNamedPolygon"), + Expression.or( + with("protomaps-basemaps:kind", "forest"), + with("protomaps-basemaps:kind", "park"), + with("protomaps-basemaps:kind", "protected_area"), + with("protomaps-basemaps:kind", "nature_reserve"), + with("protomaps-basemaps:kind", "village_green") + ), + use("minZoom", 17) + ), + rule( + with("protomaps-basemaps:hasNamedPolygon"), + Expression.or(with("protomaps-basemaps:kind", "college"), with("protomaps-basemaps:kind", "university")), + use("minZoom", 15) + ), + rule( + with("protomaps-basemaps:hasNamedPolygon"), + Expression.or( + with("protomaps-basemaps:kind", "national_park"), + with("protomaps-basemaps:kind", "aerodrome"), + with("protomaps-basemaps:kind", "golf_course"), + with("protomaps-basemaps:kind", "military"), + with("protomaps-basemaps:kind", "naval_base"), + with("protomaps-basemaps:kind", "stadium"), + with("protomaps-basemaps:kind", "zoo") + ), + use("minZoom", 14) ) )).index(); @@ -227,34 +273,29 @@ public String name() { Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d, 2); public Matcher.SourceFeatureWithComputedTags computeExtraTags(SourceFeature sf, String kind) { - Double wayArea = 0.0; - Double height = 0.0; - Boolean hasNamedPolygon = false; + Map computedTags = new HashMap<>(Map.of( + "protomaps-basemaps:kind", kind, + "protomaps-basemaps:wayArea", 0.0, + "protomaps-basemaps:height", 0.0 + )); if (sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null) { - hasNamedPolygon = true; + computedTags.put("protomaps-basemaps:hasNamedPolygon", true); try { - wayArea = sf.worldGeometry().getEnvelopeInternal().getArea() / WORLD_AREA_FOR_70K_SQUARE_METERS; + Double area = sf.worldGeometry().getEnvelopeInternal().getArea() / WORLD_AREA_FOR_70K_SQUARE_METERS; + computedTags.put("protomaps-basemaps:wayArea", area); } catch (GeometryException e) { e.log("Exception in POI way calculation"); } if (sf.hasTag("height")) { Double parsed = parseDoubleOrNull(sf.getString("height")); if (parsed != null) { - height = parsed; + computedTags.put("protomaps-basemaps:height", parsed); } } } - return new Matcher.SourceFeatureWithComputedTags( - sf, - Map.of( - "protomaps-basemaps:kind", kind, - "protomaps-basemaps:wayArea", wayArea, - "protomaps-basemaps:height", height, - "protomaps-basemaps:hasNamedPolygon", hasNamedPolygon - ) - ); + return new Matcher.SourceFeatureWithComputedTags(sf, computedTags); } public void processOsm(SourceFeature sf, FeatureCollector features) { @@ -337,7 +378,7 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { } else if (wayArea > 1) { minZoom = 13; } else if (wayArea > 0.25) { - minZoom = 14; + //minZoom = 14; } } else if (kind.equals("aerodrome") || kind.equals("golf_course") || @@ -360,7 +401,7 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { } else if (wayArea > 1) { minZoom = 13; } else if (wayArea > 0.25) { - minZoom = 14; + //minZoom = 14; } // Emphasize large international airports earlier @@ -400,7 +441,7 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { } else if (wayArea > 5) { minZoom = 14; } else { - minZoom = 15; + //minZoom = 15; } // Hack for weird San Francisco university @@ -433,7 +474,7 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { } else if (wayArea > 0.001) { minZoom = 16; } else { - minZoom = 17; + //minZoom = 17; } // Discount wilderness areas within US national forests and parks @@ -451,17 +492,17 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { } else if (wayArea > 0.01) { minZoom = 15; } else { - minZoom = 16; + //minZoom = 16; } // Typically for "building" derived label placements for shops and other businesses } else if (kind.equals("allotments")) { if (wayArea > 0.01) { minZoom = 15; } else { - minZoom = 16; + //minZoom = 16; } } else if (kind.equals("playground")) { - minZoom = 17; + // minZoom = 17; } else { if (wayArea > 10) { minZoom = 11; From 8fa32b325d093ff5fb42ce5a562d9778cb01e498 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Fri, 26 Dec 2025 14:27:26 -0800 Subject: [PATCH 08/33] Moved some protomaps-basemaps: tags to private static strings --- .../com/protomaps/basemap/layers/Pois.java | 50 ++++++++----------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index 95b63fa49..e5dbcbfa0 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -47,6 +47,10 @@ public Pois(QrankDb qrankDb) { public static final String LAYER_NAME = "pois"; + // Internal tags used to reference calculated values between matchers + private static final String KIND_ATTR = "protomaps-basemaps:kind"; + private static final String HAS_NAMED_POLYGON = "protomaps-basemaps:hasNamedPolygon"; + private static final Expression WITH_OPERATOR_USFS = with("operator", "United States Forest Service", "US Forest Service", "U.S. Forest Service", "USDA Forest Service", "United States Department of Agriculture", "US National Forest Service", "United State Forest Service", "U.S. National Forest Service"); @@ -153,7 +157,7 @@ public Pois(QrankDb qrankDb) { // Everything is zoom=15 at first rule(use("minZoom", 15)), - rule(with("protomaps-basemaps:kind", "national_park"), use("minZoom", 11)), + rule(with(KIND_ATTR, "national_park"), use("minZoom", 11)), rule(with("natural", "peak"), use("minZoom", 13)), rule(with("highway", "bus_stop"), use("minZoom", 17)), rule(with("tourism", "attraction", "camp_site", "hotel"), use("minZoom", 15)), @@ -171,7 +175,7 @@ public Pois(QrankDb qrankDb) { // Emphasize large international airports earlier rule( with("aeroway", "aerodrome"), - with("protomaps-basemaps:kind", "aerodrome"), + with(KIND_ATTR, "aerodrome"), with("iata"), use("minZoom", 11) ), @@ -217,47 +221,33 @@ public Pois(QrankDb qrankDb) { ), rule( - with("protomaps-basemaps:hasNamedPolygon"), - with("protomaps-basemaps:kind", "playground"), + with(HAS_NAMED_POLYGON), + with(KIND_ATTR, "playground"), use("minZoom", 17) ), rule( - with("protomaps-basemaps:hasNamedPolygon"), - with("protomaps-basemaps:kind", "allotments"), + with(HAS_NAMED_POLYGON), + with(KIND_ATTR, "allotments"), use("minZoom", 16) ), rule( - with("protomaps-basemaps:hasNamedPolygon"), - Expression.or(with("protomaps-basemaps:kind", "cemetery"), with("protomaps-basemaps:kind", "school")), + with(HAS_NAMED_POLYGON), + with(KIND_ATTR, "cemetery", "school"), use("minZoom", 16) ), rule( - with("protomaps-basemaps:hasNamedPolygon"), - Expression.or( - with("protomaps-basemaps:kind", "forest"), - with("protomaps-basemaps:kind", "park"), - with("protomaps-basemaps:kind", "protected_area"), - with("protomaps-basemaps:kind", "nature_reserve"), - with("protomaps-basemaps:kind", "village_green") - ), + with(HAS_NAMED_POLYGON), + with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), use("minZoom", 17) ), rule( - with("protomaps-basemaps:hasNamedPolygon"), - Expression.or(with("protomaps-basemaps:kind", "college"), with("protomaps-basemaps:kind", "university")), + with(HAS_NAMED_POLYGON), + with(KIND_ATTR, "college", "university"), use("minZoom", 15) ), rule( - with("protomaps-basemaps:hasNamedPolygon"), - Expression.or( - with("protomaps-basemaps:kind", "national_park"), - with("protomaps-basemaps:kind", "aerodrome"), - with("protomaps-basemaps:kind", "golf_course"), - with("protomaps-basemaps:kind", "military"), - with("protomaps-basemaps:kind", "naval_base"), - with("protomaps-basemaps:kind", "stadium"), - with("protomaps-basemaps:kind", "zoo") - ), + with(HAS_NAMED_POLYGON), + with(KIND_ATTR, "national_park", "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), use("minZoom", 14) ) @@ -274,13 +264,13 @@ public String name() { public Matcher.SourceFeatureWithComputedTags computeExtraTags(SourceFeature sf, String kind) { Map computedTags = new HashMap<>(Map.of( - "protomaps-basemaps:kind", kind, + KIND_ATTR, kind, "protomaps-basemaps:wayArea", 0.0, "protomaps-basemaps:height", 0.0 )); if (sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null) { - computedTags.put("protomaps-basemaps:hasNamedPolygon", true); + computedTags.put(HAS_NAMED_POLYGON, true); try { Double area = sf.worldGeometry().getEnvelopeInternal().getArea() / WORLD_AREA_FOR_70K_SQUARE_METERS; computedTags.put("protomaps-basemaps:wayArea", area); From 8200a7c40dfcddf71f5e6d9cd78e92bc7544eca7 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Fri, 26 Dec 2025 20:50:52 -0800 Subject: [PATCH 09/33] Removed HashMap --- .../com/protomaps/basemap/layers/Pois.java | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index e5dbcbfa0..254caddcb 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -22,7 +22,6 @@ import com.protomaps.basemap.feature.Matcher; import com.protomaps.basemap.feature.QrankDb; import com.protomaps.basemap.names.OsmNames; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -263,28 +262,42 @@ public String name() { Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d, 2); public Matcher.SourceFeatureWithComputedTags computeExtraTags(SourceFeature sf, String kind) { - Map computedTags = new HashMap<>(Map.of( - KIND_ATTR, kind, - "protomaps-basemaps:wayArea", 0.0, - "protomaps-basemaps:height", 0.0 - )); + Double wayArea = 0.0; + Double height = 0.0; + Boolean hasNamedPolygon = false; if (sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null) { - computedTags.put(HAS_NAMED_POLYGON, true); + hasNamedPolygon = true; try { - Double area = sf.worldGeometry().getEnvelopeInternal().getArea() / WORLD_AREA_FOR_70K_SQUARE_METERS; - computedTags.put("protomaps-basemaps:wayArea", area); + wayArea = sf.worldGeometry().getEnvelopeInternal().getArea() / WORLD_AREA_FOR_70K_SQUARE_METERS; } catch (GeometryException e) { e.log("Exception in POI way calculation"); } if (sf.hasTag("height")) { Double parsed = parseDoubleOrNull(sf.getString("height")); if (parsed != null) { - computedTags.put("protomaps-basemaps:height", parsed); + height = parsed; } } } + Map computedTags; + + if (hasNamedPolygon) { + computedTags = Map.of( + KIND_ATTR, kind, + HAS_NAMED_POLYGON, true, + "protomaps-basemaps:wayArea", wayArea, + "protomaps-basemaps:height", height + ); + } else { + computedTags = Map.of( + KIND_ATTR, kind, + "protomaps-basemaps:wayArea", wayArea, + "protomaps-basemaps:height", height + ); + } + return new Matcher.SourceFeatureWithComputedTags(sf, computedTags); } From 379cb9164371e8b27697b63c180b51e2aa229ec9 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Fri, 26 Dec 2025 21:26:32 -0800 Subject: [PATCH 10/33] Moved all college/university zooms to rules with new withinRange expression --- .../protomaps/basemap/feature/Matcher.java | 56 +++++ .../com/protomaps/basemap/layers/Pois.java | 56 ++--- .../basemap/feature/MatcherTest.java | 206 ++++++++++++++++++ 3 files changed, 283 insertions(+), 35 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java b/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java index 446250ba0..b489681d3 100644 --- a/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java +++ b/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java @@ -151,6 +151,62 @@ public static Expression without(String... arguments) { return Expression.not(with(arguments)); } + /** + * Creates an {@link Expression} that matches when a numeric tag value is within a specified range. + * + *

+ * The lower bound is exclusive (value must be greater than the lower bound). The upper bound, if provided, is + * inclusive (value must be less than or equal to the upper bound). + *

+ * + *

+ * If the upper bound is null, only the lower bound is checked (value > lowerBound). + *

+ * + *

+ * Tag values that cannot be parsed as numbers or missing tags will not match. + *

+ * + * @param tagName The name of the tag to check. + * @param lowerBound The exclusive lower bound (value must be greater than this). + * @param upperBound The inclusive upper bound (value must be less than or equal to this), or null to check only the + * lower bound. + * @return An {@link Expression} for the numeric range check. + */ + public static Expression withinRange(String tagName, Integer lowerBound, Integer upperBound) { + return new WithinRangeExpression( + tagName, + new Long(lowerBound), + (upperBound == null ? null : new Long(upperBound)) + ); + } + + /** + * Expression implementation for numeric range matching. + */ + private record WithinRangeExpression(String tagName, long lowerBound, Long upperBound) implements Expression { + + @Override + public boolean evaluate(com.onthegomap.planetiler.reader.WithTags input, List matchKeys) { + if (!input.hasTag(tagName)) { + return false; + } + long value = input.getLong(tagName); + // getLong returns 0 for invalid values, so we need to check if 0 is actually the tag value + if (value == 0 && !"0".equals(input.getString(tagName))) { + // getLong returned 0 because parsing failed + return false; + } + return value > lowerBound && (upperBound == null || value <= upperBound); + } + + @Override + public String generateJavaCode() { + return "withinRange(" + com.onthegomap.planetiler.util.Format.quote(tagName) + ", " + lowerBound + "L, " + + (upperBound == null ? "null" : upperBound + "L") + ")"; + } + } + public static Expression withPoint() { return Expression.matchGeometryType(GeometryType.POINT); } diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index 254caddcb..df404bf03 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -8,6 +8,7 @@ import static com.protomaps.basemap.feature.Matcher.use; import static com.protomaps.basemap.feature.Matcher.with; import static com.protomaps.basemap.feature.Matcher.withPoint; +import static com.protomaps.basemap.feature.Matcher.withinRange; import static com.protomaps.basemap.feature.Matcher.without; import com.onthegomap.planetiler.FeatureCollector; @@ -48,6 +49,8 @@ public Pois(QrankDb qrankDb) { // Internal tags used to reference calculated values between matchers private static final String KIND_ATTR = "protomaps-basemaps:kind"; + private static final String WAYAREA_ATTR = "protomaps-basemaps:wayArea"; + private static final String HEIGHT_ATTR = "protomaps-basemaps:height"; private static final String HAS_NAMED_POLYGON = "protomaps-basemaps:hasNamedPolygon"; private static final Expression WITH_OPERATOR_USFS = with("operator", "United States Forest Service", @@ -239,11 +242,20 @@ public Pois(QrankDb qrankDb) { with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), use("minZoom", 17) ), - rule( - with(HAS_NAMED_POLYGON), - with(KIND_ATTR, "college", "university"), - use("minZoom", 15) - ), + + // College and university polygons + + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 0, 5), use("minZoom", 15)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 5, 20), use("minZoom", 14)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 20, 50), use("minZoom", 13)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 50, 100), use("minZoom", 12)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 100, 150), use("minZoom", 11)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 150, 250), use("minZoom", 10)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 250, 5000), use("minZoom", 9)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 5000, 20000), use("minZoom", 8)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 20000, null), use("minZoom", 7)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), with("name", "Academy of Art University"), use("minZoom", 14)), // Hack for weird San Francisco university + rule( with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park", "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), @@ -287,14 +299,14 @@ public Matcher.SourceFeatureWithComputedTags computeExtraTags(SourceFeature sf, computedTags = Map.of( KIND_ATTR, kind, HAS_NAMED_POLYGON, true, - "protomaps-basemaps:wayArea", wayArea, - "protomaps-basemaps:height", height + WAYAREA_ATTR, wayArea, + HEIGHT_ATTR, height ); } else { computedTags = Map.of( KIND_ATTR, kind, - "protomaps-basemaps:wayArea", wayArea, - "protomaps-basemaps:height", height + WAYAREA_ATTR, wayArea, + HEIGHT_ATTR, height ); } @@ -425,32 +437,6 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { } } } - } else if (kind.equals("college") || - kind.equals("university")) { - if (wayArea > 20000) { - minZoom = 7; - } else if (wayArea > 5000) { - minZoom = 8; - } else if (wayArea > 250) { - minZoom = 9; - } else if (wayArea > 150) { - minZoom = 10; - } else if (wayArea > 100) { - minZoom = 11; - } else if (wayArea > 50) { - minZoom = 12; - } else if (wayArea > 20) { - minZoom = 13; - } else if (wayArea > 5) { - minZoom = 14; - } else { - //minZoom = 15; - } - - // Hack for weird San Francisco university - if (sf.getString("name").equals("Academy of Art University")) { - minZoom = 14; - } } else if (kind.equals("forest") || kind.equals("park") || kind.equals("protected_area") || diff --git a/tiles/src/test/java/com/protomaps/basemap/feature/MatcherTest.java b/tiles/src/test/java/com/protomaps/basemap/feature/MatcherTest.java index 799769549..b5a31b44d 100644 --- a/tiles/src/test/java/com/protomaps/basemap/feature/MatcherTest.java +++ b/tiles/src/test/java/com/protomaps/basemap/feature/MatcherTest.java @@ -14,11 +14,14 @@ import static com.protomaps.basemap.feature.Matcher.withLine; import static com.protomaps.basemap.feature.Matcher.withPoint; import static com.protomaps.basemap.feature.Matcher.withPolygon; +import static com.protomaps.basemap.feature.Matcher.withinRange; import static com.protomaps.basemap.feature.Matcher.without; import static com.protomaps.basemap.feature.Matcher.withoutLine; import static com.protomaps.basemap.feature.Matcher.withoutPoint; import static com.protomaps.basemap.feature.Matcher.withoutPolygon; 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.planetiler.expression.Expression; import com.onthegomap.planetiler.expression.MultiExpression; @@ -700,4 +703,207 @@ void testGetBooleanFromTag() { assertEquals(true, getBoolean(sf, matches, "a", false)); } + @Test + void testWithinRangeWithUpperBound() { + var expression = withinRange("population", 5, 10); + + // Value within range (5 < 7 <= 10) + var sf = SimpleFeature.create( + newPoint(0, 0), + Map.of("population", "7"), + "osm", + null, + 0 + ); + assertTrue(expression.evaluate(sf, List.of())); + + // Value at lower bound (not > 5) + sf = SimpleFeature.create( + newPoint(0, 0), + Map.of("population", "5"), + "osm", + null, + 0 + ); + assertFalse(expression.evaluate(sf, List.of())); + + // Value at upper bound (10 <= 10) + sf = SimpleFeature.create( + newPoint(0, 0), + Map.of("population", "10"), + "osm", + null, + 0 + ); + assertTrue(expression.evaluate(sf, List.of())); + + // Value below range + sf = SimpleFeature.create( + newPoint(0, 0), + Map.of("population", "3"), + "osm", + null, + 0 + ); + assertFalse(expression.evaluate(sf, List.of())); + + // Value above range + sf = SimpleFeature.create( + newPoint(0, 0), + Map.of("population", "15"), + "osm", + null, + 0 + ); + assertFalse(expression.evaluate(sf, List.of())); + } + + @Test + void testWithinRangeWithoutUpperBound() { + var expression = withinRange("population", 5, null); + + // Value above lower bound + var sf = SimpleFeature.create( + newPoint(0, 0), + Map.of("population", "10"), + "osm", + null, + 0 + ); + assertTrue(expression.evaluate(sf, List.of())); + + // Value at lower bound + sf = SimpleFeature.create( + newPoint(0, 0), + Map.of("population", "5"), + "osm", + null, + 0 + ); + assertFalse(expression.evaluate(sf, List.of())); + + // Value below lower bound + sf = SimpleFeature.create( + newPoint(0, 0), + Map.of("population", "3"), + "osm", + null, + 0 + ); + assertFalse(expression.evaluate(sf, List.of())); + + // Very large value + sf = SimpleFeature.create( + newPoint(0, 0), + Map.of("population", "1000000"), + "osm", + null, + 0 + ); + assertTrue(expression.evaluate(sf, List.of())); + } + + @Test + void testWithinRangeMissingTag() { + var expression = withinRange("population", 5, 10); + + var sf = SimpleFeature.create( + newPoint(0, 0), + Map.of(), + "osm", + null, + 0 + ); + assertFalse(expression.evaluate(sf, List.of())); + } + + @Test + void testWithinRangeNonNumericValue() { + var expression = withinRange("population", 5, 10); + + var sf = SimpleFeature.create( + newPoint(0, 0), + Map.of("population", "hello"), + "osm", + null, + 0 + ); + assertFalse(expression.evaluate(sf, List.of())); + } + + @Test + void testWithinRangeNegativeNumbers() { + var expression = withinRange("temperature", -10, 5); + + // Value within range (-10 < -5 <= 5) + var sf = SimpleFeature.create( + newPoint(0, 0), + Map.of("temperature", "-5"), + "osm", + null, + 0 + ); + assertTrue(expression.evaluate(sf, List.of())); + + // Value at lower bound + sf = SimpleFeature.create( + newPoint(0, 0), + Map.of("temperature", "-10"), + "osm", + null, + 0 + ); + assertFalse(expression.evaluate(sf, List.of())); + + // Value at upper bound + sf = SimpleFeature.create( + newPoint(0, 0), + Map.of("temperature", "5"), + "osm", + null, + 0 + ); + assertTrue(expression.evaluate(sf, List.of())); + } + + @Test + void testWithinRangeZeroValue() { + var expression = withinRange("value", -5, 5); + + // Zero within range (-5 < 0 <= 5) + var sf = SimpleFeature.create( + newPoint(0, 0), + Map.of("value", "0"), + "osm", + null, + 0 + ); + assertTrue(expression.evaluate(sf, List.of())); + } + + @Test + void testWithinRangeZeroAsBound() { + var expression = withinRange("value", 0, 10); + + // Value above zero bound + var sf = SimpleFeature.create( + newPoint(0, 0), + Map.of("value", "5"), + "osm", + null, + 0 + ); + assertTrue(expression.evaluate(sf, List.of())); + + // Value at zero bound + sf = SimpleFeature.create( + newPoint(0, 0), + Map.of("value", "0"), + "osm", + null, + 0 + ); + assertFalse(expression.evaluate(sf, List.of())); + } + } From d03004f7f733e79ec4f2f0ef8371a1a7ab862052 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Fri, 26 Dec 2025 23:45:25 -0800 Subject: [PATCH 11/33] Fixed zoom rule ordering failure --- tiles/src/main/java/com/protomaps/basemap/layers/Pois.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index df404bf03..d67cda2cb 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -57,7 +57,7 @@ public Pois(QrankDb qrankDb) { "US Forest Service", "U.S. Forest Service", "USDA Forest Service", "United States Department of Agriculture", "US National Forest Service", "United State Forest Service", "U.S. National Forest Service"); - private static final MultiExpression.Index> kindsIndex = MultiExpression.of(List.of( + private static final MultiExpression.Index> kindsIndex = MultiExpression.ofOrdered(List.of( // Everything is "other"/"" at first rule(use("kind", "other"), use("kindDetail", "")), @@ -154,7 +154,7 @@ public Pois(QrankDb qrankDb) { )).index(); - private static final MultiExpression.Index> zoomsIndex = MultiExpression.of(List.of( + private static final MultiExpression.Index> zoomsIndex = MultiExpression.ofOrdered(List.of( // Everything is zoom=15 at first rule(use("minZoom", 15)), @@ -437,6 +437,9 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { } } } + } else if (kind.equals("college") || + kind.equals("university")) { + // do nothing } else if (kind.equals("forest") || kind.equals("park") || kind.equals("protected_area") || From 1ff331639851db77656e7b212b11d3640eae9772 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Sat, 27 Dec 2025 09:54:08 -0800 Subject: [PATCH 12/33] Moved assorted green and other zoom-based polygons to rules --- .../com/protomaps/basemap/layers/Pois.java | 102 +++++++----------- 1 file changed, 41 insertions(+), 61 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index d67cda2cb..f2aadc50a 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -237,30 +237,47 @@ public Pois(QrankDb qrankDb) { with(KIND_ATTR, "cemetery", "school"), use("minZoom", 16) ), - rule( - with(HAS_NAMED_POLYGON), - with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), - use("minZoom", 17) - ), + + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), use("minZoom", 17)), // College and university polygons - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 0, 5), use("minZoom", 15)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 5, 20), use("minZoom", 14)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 20, 50), use("minZoom", 13)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 50, 100), use("minZoom", 12)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 100, 150), use("minZoom", 11)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 150, 250), use("minZoom", 10)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 250, 5000), use("minZoom", 9)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 5000, 20000), use("minZoom", 8)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 20000, null), use("minZoom", 7)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 0, 5000), use("minZoom", 15)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 5000, 20000), use("minZoom", 14)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 20000, 50000), use("minZoom", 13)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 50000, 100000), use("minZoom", 12)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 100000, 150000), use("minZoom", 11)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 150000, 250000), use("minZoom", 10)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 250000, 5000000), use("minZoom", 9)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 5000000, 20000000), use("minZoom", 8)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 20000000, null), use("minZoom", 7)), rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), with("name", "Academy of Art University"), use("minZoom", 14)), // Hack for weird San Francisco university - rule( - with(HAS_NAMED_POLYGON), - with(KIND_ATTR, "national_park", "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), - use("minZoom", 14) - ) + // Big green polygons + + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 0, 1), use("minZoom", 17)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 1, 10), use("minZoom", 16)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 10, 250), use("minZoom", 15)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 250, 1000), use("minZoom", 14)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 5000, 15000), use("minZoom", 12)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 15000, 250000), use("minZoom", 11)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 250000, 1000000), use("minZoom", 10)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 1000000, 4000000), use("minZoom", 9)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 4000000, 10000000), use("minZoom", 8)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 10000000, null), use("minZoom", 7)), + + // How are these similar? + + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), use("minZoom", 14)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), withinRange(WAYAREA_ATTR, 250, 1000), use("minZoom", 14)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), withinRange(WAYAREA_ATTR, 5000, 20000), use("minZoom", 12)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), withinRange(WAYAREA_ATTR, 20000, 100000), use("minZoom", 11)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), withinRange(WAYAREA_ATTR, 100000, 250000), use("minZoom", 10)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), withinRange(WAYAREA_ATTR, 250000, 5000000), use("minZoom", 9)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), withinRange(WAYAREA_ATTR, 5000000, 20000000), use("minZoom", 8)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), withinRange(WAYAREA_ATTR, 20000000, null), use("minZoom", 7)) )).index(); @@ -269,6 +286,10 @@ public String name() { return LAYER_NAME; } + // ~= pow((sqrt(70) / (4e7 / 256)) / 256, 2) ~= 4.4e-14 + private static final double WORLD_AREA_FOR_70_SQUARE_METERS = + Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70)) / 256d, 2); + // ~= pow((sqrt(7e4) / (4e7 / 256)) / 256, 2) ~= 4.4e-11 private static final double WORLD_AREA_FOR_70K_SQUARE_METERS = Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d, 2); @@ -281,7 +302,7 @@ public Matcher.SourceFeatureWithComputedTags computeExtraTags(SourceFeature sf, if (sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null) { hasNamedPolygon = true; try { - wayArea = sf.worldGeometry().getEnvelopeInternal().getArea() / WORLD_AREA_FOR_70K_SQUARE_METERS; + wayArea = sf.worldGeometry().getEnvelopeInternal().getArea() / WORLD_AREA_FOR_70_SQUARE_METERS; } catch (GeometryException e) { e.log("Exception in POI way calculation"); } @@ -401,23 +422,6 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { kind.equals("naval_base") || kind.equals("stadium") || kind.equals("zoo")) { - if (wayArea > 20000) { // 500000000 - minZoom = 7; - } else if (wayArea > 5000) { // 200000000 - minZoom = 8; - } else if (wayArea > 250) { // 40000000 - minZoom = 9; - } else if (wayArea > 100) { // 8000000 - minZoom = 10; - } else if (wayArea > 20) { // 500000 - minZoom = 11; - } else if (wayArea > 5) { - minZoom = 12; - } else if (wayArea > 1) { - minZoom = 13; - } else if (wayArea > 0.25) { - //minZoom = 14; - } // Emphasize large international airports earlier // Because the area grading resets the earlier dispensation @@ -445,30 +449,6 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { kind.equals("protected_area") || kind.equals("nature_reserve") || kind.equals("village_green")) { - if (wayArea > 10000) { - minZoom = 7; - } else if (wayArea > 4000) { - minZoom = 8; - } else if (wayArea > 1000) { - minZoom = 9; - } else if (wayArea > 250) { - minZoom = 10; - } else if (wayArea > 15) { - minZoom = 11; - } else if (wayArea > 5) { - minZoom = 12; - } else if (wayArea > 1) { - minZoom = 13; - } else if (wayArea > 0.25) { - minZoom = 14; - } else if (wayArea > 0.01) { - minZoom = 15; - } else if (wayArea > 0.001) { - minZoom = 16; - } else { - //minZoom = 17; - } - // Discount wilderness areas within US national forests and parks if (kind.equals("nature_reserve") && sf.getString("name").contains("Wilderness")) { minZoom = minZoom + 1; From f2dbdf48497ecc636365857967cbd5932ff84770 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Sat, 27 Dec 2025 10:02:49 -0800 Subject: [PATCH 13/33] Moved schools, cemeteries, and national parks to zoom-graded rules --- .../com/protomaps/basemap/layers/Pois.java | 61 +++++++------------ 1 file changed, 22 insertions(+), 39 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index f2aadc50a..89ad93920 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -232,13 +232,28 @@ public Pois(QrankDb qrankDb) { with(KIND_ATTR, "allotments"), use("minZoom", 16) ), - rule( - with(HAS_NAMED_POLYGON), - with(KIND_ATTR, "cemetery", "school"), - use("minZoom", 16) - ), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), use("minZoom", 17)), + // Schools & Cemeteries + + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "cemetery", "school"), withinRange(WAYAREA_ATTR, 0, 10), use("minZoom", 16)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "cemetery", "school"), withinRange(WAYAREA_ATTR, 10, 100), use("minZoom", 15)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "cemetery", "school"), withinRange(WAYAREA_ATTR, 100, 1000), use("minZoom", 14)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "cemetery", "school"), withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "cemetery", "school"), withinRange(WAYAREA_ATTR, 5000, null), use("minZoom", 12)), + + // National parks + + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 0, 250), use("minZoom", 17)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 250, 1000), use("minZoom", 14)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 5000, 20000), use("minZoom", 12)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 20000, 100000), use("minZoom", 11)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 100000, 250000), use("minZoom", 10)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 250000, 2000000), use("minZoom", 9)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 2000000, 10000000), use("minZoom", 8)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 10000000, 25000000), use("minZoom", 7)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 25000000, 300000000), use("minZoom", 6)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 300000000, null), use("minZoom", 5)), // College and university polygons @@ -269,7 +284,6 @@ public Pois(QrankDb qrankDb) { // How are these similar? - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), use("minZoom", 14)), rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), withinRange(WAYAREA_ATTR, 250, 1000), use("minZoom", 14)), rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), withinRange(WAYAREA_ATTR, 5000, 20000), use("minZoom", 12)), @@ -395,27 +409,7 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { // // Allowlist of kind values eligible for early zoom point labels if (kind.equals("national_park")) { - if (wayArea > 300000) { // 500000000 sq meters (web mercator proj) - minZoom = 5; - } else if (wayArea > 25000) { // 500000000 sq meters (web mercator proj) - minZoom = 6; - } else if (wayArea > 10000) { // 500000000 - minZoom = 7; - } else if (wayArea > 2000) { // 200000000 - minZoom = 8; - } else if (wayArea > 250) { // 40000000 - minZoom = 9; - } else if (wayArea > 100) { // 8000000 - minZoom = 10; - } else if (wayArea > 20) { // 500000 - minZoom = 11; - } else if (wayArea > 5) { - minZoom = 12; - } else if (wayArea > 1) { - minZoom = 13; - } else if (wayArea > 0.25) { - //minZoom = 14; - } + } else if (kind.equals("aerodrome") || kind.equals("golf_course") || kind.equals("military") || @@ -455,17 +449,6 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { } } else if (kind.equals("cemetery") || kind.equals("school")) { - if (wayArea > 5) { - minZoom = 12; - } else if (wayArea > 1) { - minZoom = 13; - } else if (wayArea > 0.1) { - minZoom = 14; - } else if (wayArea > 0.01) { - minZoom = 15; - } else { - //minZoom = 16; - } // Typically for "building" derived label placements for shops and other businesses } else if (kind.equals("allotments")) { if (wayArea > 0.01) { From cca5a645e13c99e9ca187a5f46dc091b04ab75a1 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Sat, 27 Dec 2025 10:38:33 -0800 Subject: [PATCH 14/33] Corrected withinRange() bounds logic and moved height adjustments to rules --- .../protomaps/basemap/feature/Matcher.java | 2 +- .../com/protomaps/basemap/layers/Pois.java | 87 +++++++------------ .../basemap/feature/MatcherTest.java | 12 +-- 3 files changed, 39 insertions(+), 62 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java b/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java index b489681d3..a47cc2054 100644 --- a/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java +++ b/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java @@ -197,7 +197,7 @@ public boolean evaluate(com.onthegomap.planetiler.reader.WithTags input, List lowerBound && (upperBound == null || value <= upperBound); + return value >= lowerBound && (upperBound == null || value < upperBound); } @Override diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index 89ad93920..efe9c1b9c 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -222,16 +222,39 @@ public Pois(QrankDb qrankDb) { use("minZoom", 16) ), + // Size-graded polygons, generic at first then per-kind adjustments + + rule(with(HAS_NAMED_POLYGON), withinRange(WAYAREA_ATTR, 10, 500), use("minZoom", 14)), + rule(with(HAS_NAMED_POLYGON), withinRange(WAYAREA_ATTR, 500, 2000), use("minZoom", 13)), + rule(with(HAS_NAMED_POLYGON), withinRange(WAYAREA_ATTR, 2000, 10000), use("minZoom", 12)), + rule(with(HAS_NAMED_POLYGON), withinRange(WAYAREA_ATTR, 10000, null), use("minZoom", 11)), + + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "playground"), use("minZoom", 17)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "allotments"), withinRange(WAYAREA_ATTR, 0, 10), use("minZoom", 16)), + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "allotments"), withinRange(WAYAREA_ATTR, 10, null), use("minZoom", 15)), + + // Height-graded polygons, generic at first then per-kind adjustments + // Small but tall features should show up early as they have regional prominance. + // Height measured in meters + + rule(with(HAS_NAMED_POLYGON), withinRange(WAYAREA_ATTR, 10, 2000), withinRange(HEIGHT_ATTR, 10, 20), use("minZoom", 13)), + rule(with(HAS_NAMED_POLYGON), withinRange(WAYAREA_ATTR, 10, 2000), withinRange(HEIGHT_ATTR, 20, 100), use("minZoom", 12)), + rule(with(HAS_NAMED_POLYGON), withinRange(WAYAREA_ATTR, 10, 2000), withinRange(HEIGHT_ATTR, 100, null), use("minZoom", 11)), + + // Clamp certain kind values so medium tall buildings don't crowd downtown areas + // NOTE: (nvkelso 20230623) Apply label grid to early zooms of POIs layer + // NOTE: (nvkelso 20230624) Turn this into an allowlist instead of a blocklist rule( with(HAS_NAMED_POLYGON), - with(KIND_ATTR, "playground"), - use("minZoom", 17) - ), - rule( - with(HAS_NAMED_POLYGON), - with(KIND_ATTR, "allotments"), - use("minZoom", 16) + with(KIND_ATTR, "hotel", "hostel", "parking", "bank", "place_of_worship", "jewelry", "yes", "restaurant", "coworking_space", "clothes", "art", "school"), + withinRange(WAYAREA_ATTR, 10, 2000), + withinRange(HEIGHT_ATTR, 20, 100), + use("minZoom", 13) ), + // Discount tall self storage buildings + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "storage_rental"), withinRange(WAYAREA_ATTR, 10, 2000), use("minZoom", 14)), + // Discount tall university buildings, require a related university landuse AOI + rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "university"), withinRange(WAYAREA_ATTR, 10, 2000), use("minZoom", 13)), // Schools & Cemeteries @@ -451,57 +474,11 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { kind.equals("school")) { // Typically for "building" derived label placements for shops and other businesses } else if (kind.equals("allotments")) { - if (wayArea > 0.01) { - minZoom = 15; - } else { - //minZoom = 16; - } + // } else if (kind.equals("playground")) { // minZoom = 17; } else { - if (wayArea > 10) { - minZoom = 11; - } else if (wayArea > 2) { - minZoom = 12; - } else if (wayArea > 0.5) { - minZoom = 13; - } else if (wayArea > 0.01) { - minZoom = 14; - } - - // Small but tall features should show up early as they have regional prominance. - // Height measured in meters - if (minZoom >= 13 && height > 0.0) { - if (height >= 100) { - minZoom = 11; - } else if (height >= 20) { - minZoom = 12; - } else if (height >= 10) { - minZoom = 13; - } - - // Clamp certain kind values so medium tall buildings don't crowd downtown areas - // NOTE: (nvkelso 20230623) Apply label grid to early zooms of POIs layer - // NOTE: (nvkelso 20230624) Turn this into an allowlist instead of a blocklist - if (kind.equals("hotel") || kind.equals("hostel") || kind.equals("parking") || kind.equals("bank") || - kind.equals("place_of_worship") || kind.equals("jewelry") || kind.equals("yes") || - kind.equals("restaurant") || kind.equals("coworking_space") || kind.equals("clothes") || - kind.equals("art") || kind.equals("school")) { - if (minZoom == 12) { - minZoom = 13; - } - } - - // Discount tall self storage buildings - if (kind.equals("storage_rental")) { - minZoom = 14; - } - - // Discount tall university buildings, require a related university landuse AOI - if (kind.equals("university")) { - minZoom = 13; - } - } + // } // very long text names should only be shown at later zooms diff --git a/tiles/src/test/java/com/protomaps/basemap/feature/MatcherTest.java b/tiles/src/test/java/com/protomaps/basemap/feature/MatcherTest.java index b5a31b44d..49eda5ac3 100644 --- a/tiles/src/test/java/com/protomaps/basemap/feature/MatcherTest.java +++ b/tiles/src/test/java/com/protomaps/basemap/feature/MatcherTest.java @@ -725,7 +725,7 @@ void testWithinRangeWithUpperBound() { null, 0 ); - assertFalse(expression.evaluate(sf, List.of())); + assertTrue(expression.evaluate(sf, List.of())); // Value at upper bound (10 <= 10) sf = SimpleFeature.create( @@ -735,7 +735,7 @@ void testWithinRangeWithUpperBound() { null, 0 ); - assertTrue(expression.evaluate(sf, List.of())); + assertFalse(expression.evaluate(sf, List.of())); // Value below range sf = SimpleFeature.create( @@ -780,7 +780,7 @@ void testWithinRangeWithoutUpperBound() { null, 0 ); - assertFalse(expression.evaluate(sf, List.of())); + assertTrue(expression.evaluate(sf, List.of())); // Value below lower bound sf = SimpleFeature.create( @@ -853,7 +853,7 @@ void testWithinRangeNegativeNumbers() { null, 0 ); - assertFalse(expression.evaluate(sf, List.of())); + assertTrue(expression.evaluate(sf, List.of())); // Value at upper bound sf = SimpleFeature.create( @@ -863,7 +863,7 @@ void testWithinRangeNegativeNumbers() { null, 0 ); - assertTrue(expression.evaluate(sf, List.of())); + assertFalse(expression.evaluate(sf, List.of())); } @Test @@ -903,7 +903,7 @@ void testWithinRangeZeroAsBound() { null, 0 ); - assertFalse(expression.evaluate(sf, List.of())); + assertTrue(expression.evaluate(sf, List.of())); } } From 4431fb3d0a03c651132878c3643f15c5eb275a7b Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Sat, 27 Dec 2025 10:45:42 -0800 Subject: [PATCH 15/33] Reorganized final adjustments for clarity --- .../com/protomaps/basemap/layers/Pois.java | 92 +++++-------------- 1 file changed, 23 insertions(+), 69 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index efe9c1b9c..9408d1233 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -412,85 +412,39 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { // try first for polygon -> point representations if (sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null) { - Double wayArea = 0.0; - try { - wayArea = sf.worldGeometry().getEnvelopeInternal().getArea() / WORLD_AREA_FOR_70K_SQUARE_METERS; - } catch (GeometryException e) { - e.log("Exception in POI way calculation"); - } - double height = 0.0; - if (sf.hasTag("height")) { - Double parsed = parseDoubleOrNull(sf.getString("height")); - if (parsed != null) { - height = parsed; - } - } + // Emphasize large international airports earlier + // Because the area grading resets the earlier dispensation + if (kind.equals("aerodrome")) { + if (sf.hasTag("iata")) { + // prioritize international airports over regional airports + minZoom -= 2; - // Area zoom grading overrides the kind zoom grading in the section above. - // Roughly shared with the water label area zoom grading in physical points layer - // - // Allowlist of kind values eligible for early zoom point labels - if (kind.equals("national_park")) { - - } else if (kind.equals("aerodrome") || - kind.equals("golf_course") || - kind.equals("military") || - kind.equals("naval_base") || - kind.equals("stadium") || - kind.equals("zoo")) { - - // Emphasize large international airports earlier - // Because the area grading resets the earlier dispensation - if (kind.equals("aerodrome")) { - if (sf.hasTag("iata")) { - // prioritize international airports over regional airports - minZoom -= 2; - - // but don't show international airports tooooo early - if (minZoom < 10) { - minZoom = 10; - } - } else { - // and show other airports only once their polygon begins to be visible - if (minZoom < 12) { - minZoom = 12; - } + // but don't show international airports tooooo early + if (minZoom < 10) { + minZoom = 10; + } + } else { + // and show other airports only once their polygon begins to be visible + if (minZoom < 12) { + minZoom = 12; } } - } else if (kind.equals("college") || - kind.equals("university")) { - // do nothing - } else if (kind.equals("forest") || - kind.equals("park") || - kind.equals("protected_area") || - kind.equals("nature_reserve") || - kind.equals("village_green")) { - // Discount wilderness areas within US national forests and parks - if (kind.equals("nature_reserve") && sf.getString("name").contains("Wilderness")) { - minZoom = minZoom + 1; - } - } else if (kind.equals("cemetery") || - kind.equals("school")) { - // Typically for "building" derived label placements for shops and other businesses - } else if (kind.equals("allotments")) { - // - } else if (kind.equals("playground")) { - // minZoom = 17; - } else { - // + } + + // Discount wilderness areas within US national forests and parks + if (kind.equals("nature_reserve") && sf.getString("name").contains("Wilderness")) { + minZoom += 1; } // very long text names should only be shown at later zooms if (minZoom < 14) { var nameLength = sf.getString("name").length(); - if (nameLength > 30) { - if (nameLength > 45) { - minZoom += 2; - } else { - minZoom += 1; - } + if (nameLength > 45) { + minZoom += 2; + } else if (nameLength > 30) { + minZoom += 1; } } From 1febd2e8b06f83a6854752a3711867a8d5c8e773 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Sat, 27 Dec 2025 10:52:14 -0800 Subject: [PATCH 16/33] Shorthanded a bunch of zoom rules --- .../com/protomaps/basemap/layers/Pois.java | 134 ++++++++++-------- 1 file changed, 75 insertions(+), 59 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index 9408d1233..64b399e21 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -154,6 +154,18 @@ public Pois(QrankDb qrankDb) { )).index(); + private static final Expression with_named_polygon = with(HAS_NAMED_POLYGON); + private static final Expression with_s_c_named_poly = + Expression.and(with_named_polygon, with(KIND_ATTR, "cemetery", "school")); + private static final Expression with_n_p_named_poly = + Expression.and(with_named_polygon, with(KIND_ATTR, "national_park")); + private static final Expression with_c_u_named_poly = + Expression.and(with_named_polygon, with(KIND_ATTR, "college", "university")); + private static final Expression with_b_g_named_poly = Expression.and(with_named_polygon, + with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green")); + private static final Expression with_etc_named_poly = Expression.and(with_named_polygon, + with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo")); + private static final MultiExpression.Index> zoomsIndex = MultiExpression.ofOrdered(List.of( // Everything is zoom=15 at first @@ -224,97 +236,101 @@ public Pois(QrankDb qrankDb) { // Size-graded polygons, generic at first then per-kind adjustments - rule(with(HAS_NAMED_POLYGON), withinRange(WAYAREA_ATTR, 10, 500), use("minZoom", 14)), - rule(with(HAS_NAMED_POLYGON), withinRange(WAYAREA_ATTR, 500, 2000), use("minZoom", 13)), - rule(with(HAS_NAMED_POLYGON), withinRange(WAYAREA_ATTR, 2000, 10000), use("minZoom", 12)), - rule(with(HAS_NAMED_POLYGON), withinRange(WAYAREA_ATTR, 10000, null), use("minZoom", 11)), + rule(with_named_polygon, withinRange(WAYAREA_ATTR, 10, 500), use("minZoom", 14)), + rule(with_named_polygon, withinRange(WAYAREA_ATTR, 500, 2000), use("minZoom", 13)), + rule(with_named_polygon, withinRange(WAYAREA_ATTR, 2000, 10000), use("minZoom", 12)), + rule(with_named_polygon, withinRange(WAYAREA_ATTR, 10000, null), use("minZoom", 11)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "playground"), use("minZoom", 17)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "allotments"), withinRange(WAYAREA_ATTR, 0, 10), use("minZoom", 16)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "allotments"), withinRange(WAYAREA_ATTR, 10, null), use("minZoom", 15)), + rule(with_named_polygon, with(KIND_ATTR, "playground"), use("minZoom", 17)), + rule(with_named_polygon, with(KIND_ATTR, "allotments"), withinRange(WAYAREA_ATTR, 0, 10), use("minZoom", 16)), + rule(with_named_polygon, with(KIND_ATTR, "allotments"), withinRange(WAYAREA_ATTR, 10, null), use("minZoom", 15)), // Height-graded polygons, generic at first then per-kind adjustments // Small but tall features should show up early as they have regional prominance. // Height measured in meters - rule(with(HAS_NAMED_POLYGON), withinRange(WAYAREA_ATTR, 10, 2000), withinRange(HEIGHT_ATTR, 10, 20), use("minZoom", 13)), - rule(with(HAS_NAMED_POLYGON), withinRange(WAYAREA_ATTR, 10, 2000), withinRange(HEIGHT_ATTR, 20, 100), use("minZoom", 12)), - rule(with(HAS_NAMED_POLYGON), withinRange(WAYAREA_ATTR, 10, 2000), withinRange(HEIGHT_ATTR, 100, null), use("minZoom", 11)), + rule(with_named_polygon, withinRange(WAYAREA_ATTR, 10, 2000), withinRange(HEIGHT_ATTR, 10, 20), use("minZoom", 13)), + rule(with_named_polygon, withinRange(WAYAREA_ATTR, 10, 2000), withinRange(HEIGHT_ATTR, 20, 100), + use("minZoom", 12)), + rule(with_named_polygon, withinRange(WAYAREA_ATTR, 10, 2000), withinRange(HEIGHT_ATTR, 100, null), + use("minZoom", 11)), // Clamp certain kind values so medium tall buildings don't crowd downtown areas // NOTE: (nvkelso 20230623) Apply label grid to early zooms of POIs layer // NOTE: (nvkelso 20230624) Turn this into an allowlist instead of a blocklist rule( - with(HAS_NAMED_POLYGON), - with(KIND_ATTR, "hotel", "hostel", "parking", "bank", "place_of_worship", "jewelry", "yes", "restaurant", "coworking_space", "clothes", "art", "school"), + with_named_polygon, + with(KIND_ATTR, "hotel", "hostel", "parking", "bank", "place_of_worship", "jewelry", "yes", "restaurant", + "coworking_space", "clothes", "art", "school"), withinRange(WAYAREA_ATTR, 10, 2000), withinRange(HEIGHT_ATTR, 20, 100), use("minZoom", 13) ), // Discount tall self storage buildings - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "storage_rental"), withinRange(WAYAREA_ATTR, 10, 2000), use("minZoom", 14)), + rule(with_named_polygon, with(KIND_ATTR, "storage_rental"), withinRange(WAYAREA_ATTR, 10, 2000), + use("minZoom", 14)), // Discount tall university buildings, require a related university landuse AOI - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "university"), withinRange(WAYAREA_ATTR, 10, 2000), use("minZoom", 13)), + rule(with_named_polygon, with(KIND_ATTR, "university"), withinRange(WAYAREA_ATTR, 10, 2000), use("minZoom", 13)), // Schools & Cemeteries - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "cemetery", "school"), withinRange(WAYAREA_ATTR, 0, 10), use("minZoom", 16)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "cemetery", "school"), withinRange(WAYAREA_ATTR, 10, 100), use("minZoom", 15)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "cemetery", "school"), withinRange(WAYAREA_ATTR, 100, 1000), use("minZoom", 14)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "cemetery", "school"), withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "cemetery", "school"), withinRange(WAYAREA_ATTR, 5000, null), use("minZoom", 12)), + rule(with_s_c_named_poly, withinRange(WAYAREA_ATTR, 0, 10), use("minZoom", 16)), + rule(with_s_c_named_poly, withinRange(WAYAREA_ATTR, 10, 100), use("minZoom", 15)), + rule(with_s_c_named_poly, withinRange(WAYAREA_ATTR, 100, 1000), use("minZoom", 14)), + rule(with_s_c_named_poly, withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), + rule(with_s_c_named_poly, withinRange(WAYAREA_ATTR, 5000, null), use("minZoom", 12)), // National parks - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 0, 250), use("minZoom", 17)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 250, 1000), use("minZoom", 14)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 5000, 20000), use("minZoom", 12)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 20000, 100000), use("minZoom", 11)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 100000, 250000), use("minZoom", 10)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 250000, 2000000), use("minZoom", 9)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 2000000, 10000000), use("minZoom", 8)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 10000000, 25000000), use("minZoom", 7)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 25000000, 300000000), use("minZoom", 6)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "national_park"), withinRange(WAYAREA_ATTR, 300000000, null), use("minZoom", 5)), + rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 0, 250), use("minZoom", 17)), + rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 250, 1000), use("minZoom", 14)), + rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), + rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 5000, 20000), use("minZoom", 12)), + rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 20000, 100000), use("minZoom", 11)), + rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 100000, 250000), use("minZoom", 10)), + rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 250000, 2000000), use("minZoom", 9)), + rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 2000000, 10000000), use("minZoom", 8)), + rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 10000000, 25000000), use("minZoom", 7)), + rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 25000000, 300000000), use("minZoom", 6)), + rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 300000000, null), use("minZoom", 5)), // College and university polygons - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 0, 5000), use("minZoom", 15)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 5000, 20000), use("minZoom", 14)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 20000, 50000), use("minZoom", 13)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 50000, 100000), use("minZoom", 12)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 100000, 150000), use("minZoom", 11)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 150000, 250000), use("minZoom", 10)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 250000, 5000000), use("minZoom", 9)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 5000000, 20000000), use("minZoom", 8)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), withinRange(WAYAREA_ATTR, 20000000, null), use("minZoom", 7)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "college", "university"), with("name", "Academy of Art University"), use("minZoom", 14)), // Hack for weird San Francisco university + rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 0, 5000), use("minZoom", 15)), + rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 5000, 20000), use("minZoom", 14)), + rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 20000, 50000), use("minZoom", 13)), + rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 50000, 100000), use("minZoom", 12)), + rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 100000, 150000), use("minZoom", 11)), + rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 150000, 250000), use("minZoom", 10)), + rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 250000, 5000000), use("minZoom", 9)), + rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 5000000, 20000000), use("minZoom", 8)), + rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 20000000, null), use("minZoom", 7)), + rule(with_c_u_named_poly, with("name", "Academy of Art University"), use("minZoom", 14)), // Hack for weird San Francisco university // Big green polygons - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 0, 1), use("minZoom", 17)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 1, 10), use("minZoom", 16)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 10, 250), use("minZoom", 15)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 250, 1000), use("minZoom", 14)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 5000, 15000), use("minZoom", 12)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 15000, 250000), use("minZoom", 11)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 250000, 1000000), use("minZoom", 10)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 1000000, 4000000), use("minZoom", 9)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 4000000, 10000000), use("minZoom", 8)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green"), withinRange(WAYAREA_ATTR, 10000000, null), use("minZoom", 7)), + rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 0, 1), use("minZoom", 17)), + rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 1, 10), use("minZoom", 16)), + rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 10, 250), use("minZoom", 15)), + rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 250, 1000), use("minZoom", 14)), + rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), + rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 5000, 15000), use("minZoom", 12)), + rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 15000, 250000), use("minZoom", 11)), + rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 250000, 1000000), use("minZoom", 10)), + rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 1000000, 4000000), use("minZoom", 9)), + rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 4000000, 10000000), use("minZoom", 8)), + rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 10000000, null), use("minZoom", 7)), // How are these similar? - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), withinRange(WAYAREA_ATTR, 250, 1000), use("minZoom", 14)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), withinRange(WAYAREA_ATTR, 5000, 20000), use("minZoom", 12)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), withinRange(WAYAREA_ATTR, 20000, 100000), use("minZoom", 11)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), withinRange(WAYAREA_ATTR, 100000, 250000), use("minZoom", 10)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), withinRange(WAYAREA_ATTR, 250000, 5000000), use("minZoom", 9)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), withinRange(WAYAREA_ATTR, 5000000, 20000000), use("minZoom", 8)), - rule(with(HAS_NAMED_POLYGON), with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"), withinRange(WAYAREA_ATTR, 20000000, null), use("minZoom", 7)) + rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 250, 1000), use("minZoom", 14)), + rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), + rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 5000, 20000), use("minZoom", 12)), + rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 20000, 100000), use("minZoom", 11)), + rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 100000, 250000), use("minZoom", 10)), + rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 250000, 5000000), use("minZoom", 9)), + rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 5000000, 20000000), use("minZoom", 8)), + rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 20000000, null), use("minZoom", 7)) )).index(); From 1a59b25ab7834df00a0fb7f0c5b4fdcf38d99351 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Sat, 27 Dec 2025 11:09:50 -0800 Subject: [PATCH 17/33] Switched to overloaded withinRange() to allow for scientific notation in code --- .../protomaps/basemap/feature/Matcher.java | 45 +++++++++--- .../com/protomaps/basemap/layers/Pois.java | 70 +++++++++---------- .../basemap/feature/MatcherTest.java | 2 +- 3 files changed, 68 insertions(+), 49 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java b/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java index a47cc2054..c81fd7d8a 100644 --- a/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java +++ b/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java @@ -155,12 +155,11 @@ public static Expression without(String... arguments) { * Creates an {@link Expression} that matches when a numeric tag value is within a specified range. * *

- * The lower bound is exclusive (value must be greater than the lower bound). The upper bound, if provided, is - * inclusive (value must be less than or equal to the upper bound). + * The lower bound is inclusive. The upper bound, if provided, is exclusive. *

* *

- * If the upper bound is null, only the lower bound is checked (value > lowerBound). + * If the upper bound is null, only the lower bound is checked (value >= lowerBound). *

* *

@@ -168,17 +167,41 @@ public static Expression without(String... arguments) { *

* * @param tagName The name of the tag to check. - * @param lowerBound The exclusive lower bound (value must be greater than this). - * @param upperBound The inclusive upper bound (value must be less than or equal to this), or null to check only the - * lower bound. + * @param lowerBound The inclusive lower bound. + * @param upperBound The exclusive upper bound, or null to check only the lower bound. * @return An {@link Expression} for the numeric range check. */ public static Expression withinRange(String tagName, Integer lowerBound, Integer upperBound) { - return new WithinRangeExpression( - tagName, - new Long(lowerBound), - (upperBound == null ? null : new Long(upperBound)) - ); + return new WithinRangeExpression(tagName, new Long(lowerBound), new Long(upperBound)); + } + + /** + * Overload withinRange to accept just lower bound integer + */ + public static Expression withinRange(String tagName, Integer lowerBound) { + return new WithinRangeExpression(tagName, new Long(lowerBound), null); + } + + /** + * Overload withinRange to accept lower bound integer and upper bound double + */ + public static Expression withinRange(String tagName, Integer lowerBound, Double upperBound) { + return new WithinRangeExpression(tagName, new Long(lowerBound), Double.valueOf(upperBound).longValue()); + } + + /** + * Overload withinRange to accept bounds as doubles + */ + public static Expression withinRange(String tagName, Double lowerBound, Double upperBound) { + return new WithinRangeExpression(tagName, Double.valueOf(lowerBound).longValue(), + Double.valueOf(upperBound).longValue()); + } + + /** + * Overload withinRange to accept just lower bound double + */ + public static Expression withinRange(String tagName, Double lowerBound) { + return new WithinRangeExpression(tagName, Double.valueOf(lowerBound).longValue(), null); } /** diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index 64b399e21..e4ae3b01f 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -238,12 +238,12 @@ public Pois(QrankDb qrankDb) { rule(with_named_polygon, withinRange(WAYAREA_ATTR, 10, 500), use("minZoom", 14)), rule(with_named_polygon, withinRange(WAYAREA_ATTR, 500, 2000), use("minZoom", 13)), - rule(with_named_polygon, withinRange(WAYAREA_ATTR, 2000, 10000), use("minZoom", 12)), - rule(with_named_polygon, withinRange(WAYAREA_ATTR, 10000, null), use("minZoom", 11)), + rule(with_named_polygon, withinRange(WAYAREA_ATTR, 2000, 1e4), use("minZoom", 12)), + rule(with_named_polygon, withinRange(WAYAREA_ATTR, 1e4), use("minZoom", 11)), rule(with_named_polygon, with(KIND_ATTR, "playground"), use("minZoom", 17)), rule(with_named_polygon, with(KIND_ATTR, "allotments"), withinRange(WAYAREA_ATTR, 0, 10), use("minZoom", 16)), - rule(with_named_polygon, with(KIND_ATTR, "allotments"), withinRange(WAYAREA_ATTR, 10, null), use("minZoom", 15)), + rule(with_named_polygon, with(KIND_ATTR, "allotments"), withinRange(WAYAREA_ATTR, 10), use("minZoom", 15)), // Height-graded polygons, generic at first then per-kind adjustments // Small but tall features should show up early as they have regional prominance. @@ -252,7 +252,7 @@ public Pois(QrankDb qrankDb) { rule(with_named_polygon, withinRange(WAYAREA_ATTR, 10, 2000), withinRange(HEIGHT_ATTR, 10, 20), use("minZoom", 13)), rule(with_named_polygon, withinRange(WAYAREA_ATTR, 10, 2000), withinRange(HEIGHT_ATTR, 20, 100), use("minZoom", 12)), - rule(with_named_polygon, withinRange(WAYAREA_ATTR, 10, 2000), withinRange(HEIGHT_ATTR, 100, null), + rule(with_named_polygon, withinRange(WAYAREA_ATTR, 10, 2000), withinRange(HEIGHT_ATTR, 100), use("minZoom", 11)), // Clamp certain kind values so medium tall buildings don't crowd downtown areas @@ -278,33 +278,33 @@ public Pois(QrankDb qrankDb) { rule(with_s_c_named_poly, withinRange(WAYAREA_ATTR, 10, 100), use("minZoom", 15)), rule(with_s_c_named_poly, withinRange(WAYAREA_ATTR, 100, 1000), use("minZoom", 14)), rule(with_s_c_named_poly, withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), - rule(with_s_c_named_poly, withinRange(WAYAREA_ATTR, 5000, null), use("minZoom", 12)), + rule(with_s_c_named_poly, withinRange(WAYAREA_ATTR, 5000), use("minZoom", 12)), // National parks rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 0, 250), use("minZoom", 17)), rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 250, 1000), use("minZoom", 14)), rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), - rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 5000, 20000), use("minZoom", 12)), - rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 20000, 100000), use("minZoom", 11)), - rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 100000, 250000), use("minZoom", 10)), - rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 250000, 2000000), use("minZoom", 9)), - rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 2000000, 10000000), use("minZoom", 8)), - rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 10000000, 25000000), use("minZoom", 7)), - rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 25000000, 300000000), use("minZoom", 6)), - rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 300000000, null), use("minZoom", 5)), + rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 5000, 2e4), use("minZoom", 12)), + rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 2e4, 1e5), use("minZoom", 11)), + rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 1e5, 2.5e5), use("minZoom", 10)), + rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 2.5e5, 2e6), use("minZoom", 9)), + rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 2e6, 1e7), use("minZoom", 8)), + rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 1e7, 2.5e7), use("minZoom", 7)), + rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 2.5e7, 3e8), use("minZoom", 6)), + rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 3e8), use("minZoom", 5)), // College and university polygons rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 0, 5000), use("minZoom", 15)), - rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 5000, 20000), use("minZoom", 14)), - rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 20000, 50000), use("minZoom", 13)), - rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 50000, 100000), use("minZoom", 12)), - rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 100000, 150000), use("minZoom", 11)), - rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 150000, 250000), use("minZoom", 10)), - rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 250000, 5000000), use("minZoom", 9)), - rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 5000000, 20000000), use("minZoom", 8)), - rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 20000000, null), use("minZoom", 7)), + rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 5000, 2e4), use("minZoom", 14)), + rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 2e4, 5e4), use("minZoom", 13)), + rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 5e4, 1e5), use("minZoom", 12)), + rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 1e5, 1.5e5), use("minZoom", 11)), + rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 1.5e5, 2.5e5), use("minZoom", 10)), + rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 2.5e5, 5e6), use("minZoom", 9)), + rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 5e6, 2e7), use("minZoom", 8)), + rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 2e7), use("minZoom", 7)), rule(with_c_u_named_poly, with("name", "Academy of Art University"), use("minZoom", 14)), // Hack for weird San Francisco university // Big green polygons @@ -314,23 +314,23 @@ public Pois(QrankDb qrankDb) { rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 10, 250), use("minZoom", 15)), rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 250, 1000), use("minZoom", 14)), rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), - rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 5000, 15000), use("minZoom", 12)), - rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 15000, 250000), use("minZoom", 11)), - rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 250000, 1000000), use("minZoom", 10)), - rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 1000000, 4000000), use("minZoom", 9)), - rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 4000000, 10000000), use("minZoom", 8)), - rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 10000000, null), use("minZoom", 7)), + rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 5000, 1.5e4), use("minZoom", 12)), + rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 1.5e4, 2.5e5), use("minZoom", 11)), + rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 2.5e5, 1e6), use("minZoom", 10)), + rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 1e6, 4e6), use("minZoom", 9)), + rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 4e6, 1e7), use("minZoom", 8)), + rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 1e7), use("minZoom", 7)), // How are these similar? rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 250, 1000), use("minZoom", 14)), rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), - rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 5000, 20000), use("minZoom", 12)), - rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 20000, 100000), use("minZoom", 11)), - rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 100000, 250000), use("minZoom", 10)), - rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 250000, 5000000), use("minZoom", 9)), - rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 5000000, 20000000), use("minZoom", 8)), - rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 20000000, null), use("minZoom", 7)) + rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 5000, 2e4), use("minZoom", 12)), + rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 2e4, 1e5), use("minZoom", 11)), + rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 1e5, 2.5e5), use("minZoom", 10)), + rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 2.5e5, 5e6), use("minZoom", 9)), + rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 5e6, 2e7), use("minZoom", 8)), + rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 2e7), use("minZoom", 7)) )).index(); @@ -343,10 +343,6 @@ public String name() { private static final double WORLD_AREA_FOR_70_SQUARE_METERS = Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70)) / 256d, 2); - // ~= pow((sqrt(7e4) / (4e7 / 256)) / 256, 2) ~= 4.4e-11 - private static final double WORLD_AREA_FOR_70K_SQUARE_METERS = - Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d, 2); - public Matcher.SourceFeatureWithComputedTags computeExtraTags(SourceFeature sf, String kind) { Double wayArea = 0.0; Double height = 0.0; diff --git a/tiles/src/test/java/com/protomaps/basemap/feature/MatcherTest.java b/tiles/src/test/java/com/protomaps/basemap/feature/MatcherTest.java index 49eda5ac3..4fd641bac 100644 --- a/tiles/src/test/java/com/protomaps/basemap/feature/MatcherTest.java +++ b/tiles/src/test/java/com/protomaps/basemap/feature/MatcherTest.java @@ -760,7 +760,7 @@ void testWithinRangeWithUpperBound() { @Test void testWithinRangeWithoutUpperBound() { - var expression = withinRange("population", 5, null); + var expression = withinRange("population", 5); // Value above lower bound var sf = SimpleFeature.create( From 665645a7322a66c4c34745c367a571423b379542 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Sat, 27 Dec 2025 11:58:12 -0800 Subject: [PATCH 18/33] Moved last top-level tag checks in processOsm() to rules --- .../com/protomaps/basemap/layers/Pois.java | 285 +++++++++--------- 1 file changed, 138 insertions(+), 147 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index e4ae3b01f..bfac12875 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -168,8 +168,29 @@ public Pois(QrankDb qrankDb) { private static final MultiExpression.Index> zoomsIndex = MultiExpression.ofOrdered(List.of( - // Everything is zoom=15 at first - rule(use("minZoom", 15)), + // Everything with a point or a valid tag is zoom=15 at first + rule( + Expression.or( + withPoint(), + with("aeroway", "aerodrome"), + with("amenity"), + with("attraction"), + with("boundary", "national_park", "protected_area"), + with("craft"), + with("highway", "bus_stop"), + with("historic"), + with("landuse", "cemetery", "recreation_ground", "winter_sports", "quarry", "park", "forest", "military", + "village_green", "allotments"), + with("leisure"), + with("natural", "beach", "peak"), + with("railway", "station"), + with("shop"), + Expression.and(with("tourism"), without("historic", "district")) + ), + use("minZoom", 15) + ), + + // Fine-tune lots of specific categories rule(with(KIND_ATTR, "national_park"), use("minZoom", 11)), rule(with("natural", "peak"), use("minZoom", 13)), @@ -185,14 +206,7 @@ public Pois(QrankDb qrankDb) { rule(with("amenity", "hospital"), use("minZoom", 12)), rule(with("amenity", "university", "college"), use("minZoom", 14)), // One would think University should be earlier, but there are lots of dinky node only places, so if the university has a large area, it'll naturally improve its zoom in another section... rule(with("aeroway", "aerodrome"), use("minZoom", 13)), - - // Emphasize large international airports earlier - rule( - with("aeroway", "aerodrome"), - with(KIND_ATTR, "aerodrome"), - with("iata"), - use("minZoom", 11) - ), + rule(with("aeroway", "aerodrome"), with(KIND_ATTR, "aerodrome"), with("iata"), use("minZoom", 11)), // Emphasize large international airports earlier rule( withPoint(), @@ -216,6 +230,7 @@ public Pois(QrankDb qrankDb) { ), // Some features should only be visible at very late zooms when they don't have a name + rule( withPoint(), without("name"), @@ -321,7 +336,7 @@ public Pois(QrankDb qrankDb) { rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 4e6, 1e7), use("minZoom", 8)), rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 1e7), use("minZoom", 7)), - // How are these similar? + // Remaining grab-bag of scaled kinds rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 250, 1000), use("minZoom", 14)), rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), @@ -366,176 +381,152 @@ public Matcher.SourceFeatureWithComputedTags computeExtraTags(SourceFeature sf, Map computedTags; if (hasNamedPolygon) { - computedTags = Map.of( - KIND_ATTR, kind, - HAS_NAMED_POLYGON, true, - WAYAREA_ATTR, wayArea, - HEIGHT_ATTR, height - ); + computedTags = Map.of(KIND_ATTR, kind, WAYAREA_ATTR, wayArea, HEIGHT_ATTR, height, HAS_NAMED_POLYGON, true); } else { - computedTags = Map.of( - KIND_ATTR, kind, - WAYAREA_ATTR, wayArea, - HEIGHT_ATTR, height - ); + computedTags = Map.of(KIND_ATTR, kind, WAYAREA_ATTR, wayArea, HEIGHT_ATTR, height); } return new Matcher.SourceFeatureWithComputedTags(sf, computedTags); } public void processOsm(SourceFeature sf, FeatureCollector features) { + // We only do points and polygons for POI labels + if (!sf.isPoint() && !sf.canBePolygon()) + return; + + // Map the Protomaps "kind" classification to incoming tags var kindMatches = kindsIndex.getMatches(sf); - if (kindMatches.isEmpty()) { + if (kindMatches.isEmpty()) return; - } // Calculate dimensions and create a wrapper with computed tags var sf2 = computeExtraTags(sf, getString(sf, kindMatches, "kind", "undefined")); var zoomMatches = zoomsIndex.getMatches(sf2); - if (zoomMatches.isEmpty()) { + if (zoomMatches.isEmpty()) return; - } String kind = getString(sf2, kindMatches, "kind", "undefined"); String kindDetail = getString(sf2, kindMatches, "kindDetail", "undefined"); Integer minZoom = getInteger(sf2, zoomMatches, "minZoom", 99); - if ((sf.isPoint() || sf.canBePolygon()) && (sf.hasTag("aeroway", "aerodrome") || - sf.hasTag("amenity") || - sf.hasTag("attraction") || - sf.hasTag("boundary", "national_park", "protected_area") || - sf.hasTag("craft") || - sf.hasTag("historic") || - sf.hasTag("landuse", "cemetery", "recreation_ground", "winter_sports", "quarry", "park", "forest", "military", - "village_green", "allotments") || - sf.hasTag("leisure") || - sf.hasTag("natural", "beach", "peak") || - sf.hasTag("railway", "station") || - sf.hasTag("highway", "bus_stop") || - sf.hasTag("shop") || - sf.hasTag("tourism") && - (!sf.hasTag("historic", "district")))) { - long qrank = 0; - - String wikidata = sf.getString("wikidata"); - if (wikidata != null) { - qrank = qrankDb.get(wikidata); - } + long qrank = 0; + + String wikidata = sf.getString("wikidata"); + if (wikidata != null) { + qrank = qrankDb.get(wikidata); + } - // try first for polygon -> point representations - if (sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null) { - - // Emphasize large international airports earlier - // Because the area grading resets the earlier dispensation - if (kind.equals("aerodrome")) { - if (sf.hasTag("iata")) { - // prioritize international airports over regional airports - minZoom -= 2; - - // but don't show international airports tooooo early - if (minZoom < 10) { - minZoom = 10; - } - } else { - // and show other airports only once their polygon begins to be visible - if (minZoom < 12) { - minZoom = 12; - } + // try first for polygon -> point representations + if (sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null) { + // Emphasize large international airports earlier + // Because the area grading resets the earlier dispensation + if (kind.equals("aerodrome")) { + if (sf.hasTag("iata")) { + // prioritize international airports over regional airports + minZoom -= 2; + + // but don't show international airports tooooo early + if (minZoom < 10) { + minZoom = 10; + } + } else { + // and show other airports only once their polygon begins to be visible + if (minZoom < 12) { + minZoom = 12; } } + } - // Discount wilderness areas within US national forests and parks - if (kind.equals("nature_reserve") && sf.getString("name").contains("Wilderness")) { - minZoom += 1; - } + // Discount wilderness areas within US national forests and parks + if (kind.equals("nature_reserve") && sf.getString("name").contains("Wilderness")) { + minZoom += 1; + } - // very long text names should only be shown at later zooms - if (minZoom < 14) { - var nameLength = sf.getString("name").length(); + // very long text names should only be shown at later zooms + if (minZoom < 14) { + var nameLength = sf.getString("name").length(); - if (nameLength > 45) { - minZoom += 2; - } else if (nameLength > 30) { - minZoom += 1; - } + if (nameLength > 45) { + minZoom += 2; + } else if (nameLength > 30) { + minZoom += 1; } + } - var rankedZoom = QrankDb.assignZoom(qrankGrading, kind, qrank); - if (rankedZoom.isPresent()) - minZoom = rankedZoom.get(); - - var polyLabelPosition = features.pointOnSurface(this.name()) - // all POIs should receive their IDs at all zooms - // (there is no merging of POIs like with lines and polygons in other layers) - .setId(FeatureId.create(sf)) - // Core Tilezen schema properties - .setAttr("kind", kind) - // While other layers don't need min_zoom, POIs do for more predictable client-side label collisions - // 512 px zooms versus 256 px logical zooms - .setAttr("min_zoom", minZoom + 1) - // - // DEBUG - //.setAttr("area_debug", wayArea) - // - // Core OSM tags for different kinds of places - // Special airport only tag (to indicate if it's an airport with regular commercial flights) - .setAttr("iata", sf.getString("iata")) - .setAttr("elevation", sf.getString("ele")) - // Extra OSM tags for certain kinds of places - // These are duplicate of what's in the kind_detail tag - .setBufferPixels(8) - .setZoomRange(Math.min(15, minZoom), 15); + var rankedZoom = QrankDb.assignZoom(qrankGrading, kind, qrank); + if (rankedZoom.isPresent()) + minZoom = rankedZoom.get(); + var polyLabelPosition = features.pointOnSurface(this.name()) + // all POIs should receive their IDs at all zooms + // (there is no merging of POIs like with lines and polygons in other layers) + .setId(FeatureId.create(sf)) // Core Tilezen schema properties - if (!kindDetail.isEmpty()) { - polyLabelPosition.setAttr("kind_detail", kindDetail); - } + .setAttr("kind", kind) + // While other layers don't need min_zoom, POIs do for more predictable client-side label collisions + // 512 px zooms versus 256 px logical zooms + .setAttr("min_zoom", minZoom + 1) + // + // DEBUG + //.setAttr("area_debug", wayArea) + // + // Core OSM tags for different kinds of places + // Special airport only tag (to indicate if it's an airport with regular commercial flights) + .setAttr("iata", sf.getString("iata")) + .setAttr("elevation", sf.getString("ele")) + // Extra OSM tags for certain kinds of places + // These are duplicate of what's in the kind_detail tag + .setBufferPixels(8) + .setZoomRange(Math.min(15, minZoom), 15); + + // Core Tilezen schema properties + if (!kindDetail.isEmpty()) { + polyLabelPosition.setAttr("kind_detail", kindDetail); + } - OsmNames.setOsmNames(polyLabelPosition, sf, 0); - - // Server sort features so client label collisions are pre-sorted - // NOTE: (nvkelso 20230627) This could also include other params like the name - polyLabelPosition.setSortKey(minZoom * 1000); - - // Even with the categorical zoom bucketing above, we end up with too dense a point feature spread in downtown - // areas, so cull the labels which wouldn't label at earlier zooms than the max_zoom of 15 - polyLabelPosition.setPointLabelGridSizeAndLimit(14, 8, 1); - - } else if (sf.isPoint()) { - var rankedZoom = QrankDb.assignZoom(qrankGrading, kind, qrank); - if (rankedZoom.isPresent()) - minZoom = rankedZoom.get(); - - var pointFeature = features.point(this.name()) - // all POIs should receive their IDs at all zooms - // (there is no merging of POIs like with lines and polygons in other layers) - .setId(FeatureId.create(sf)) - // Core Tilezen schema properties - .setAttr("kind", kind) - // While other layers don't need min_zoom, POIs do for more predictable client-side label collisions - // 512 px zooms versus 256 px logical zooms - .setAttr("min_zoom", minZoom + 1) - // Core OSM tags for different kinds of places - // Special airport only tag (to indicate if it's an airport with regular commercial flights) - .setAttr("iata", sf.getString("iata")) - .setBufferPixels(8) - .setZoomRange(Math.min(minZoom, 15), 15); + OsmNames.setOsmNames(polyLabelPosition, sf, 0); - // Core Tilezen schema properties - if (!kindDetail.isEmpty()) { - pointFeature.setAttr("kind_detail", kindDetail); - } + // Server sort features so client label collisions are pre-sorted + // NOTE: (nvkelso 20230627) This could also include other params like the name + polyLabelPosition.setSortKey(minZoom * 1000); - OsmNames.setOsmNames(pointFeature, sf, 0); + // Even with the categorical zoom bucketing above, we end up with too dense a point feature spread in downtown + // areas, so cull the labels which wouldn't label at earlier zooms than the max_zoom of 15 + polyLabelPosition.setPointLabelGridSizeAndLimit(14, 8, 1); - // Server sort features so client label collisions are pre-sorted - // NOTE: (nvkelso 20230627) This could also include other params like the name - pointFeature.setSortKey(minZoom * 1000); + } else if (sf.isPoint()) { + var rankedZoom = QrankDb.assignZoom(qrankGrading, kind, qrank); + if (rankedZoom.isPresent()) + minZoom = rankedZoom.get(); - // Even with the categorical zoom bucketing above, we end up with too dense a point feature spread in downtown - // areas, so cull the labels which wouldn't label at earlier zooms than the max_zoom of 15 - pointFeature.setPointLabelGridSizeAndLimit(14, 8, 1); - } + var pointFeature = features.point(this.name()) + // all POIs should receive their IDs at all zooms + // (there is no merging of POIs like with lines and polygons in other layers) + .setId(FeatureId.create(sf)) + // Core Tilezen schema properties + .setAttr("kind", kind) + // While other layers don't need min_zoom, POIs do for more predictable client-side label collisions + // 512 px zooms versus 256 px logical zooms + .setAttr("min_zoom", minZoom + 1) + // Core OSM tags for different kinds of places + // Special airport only tag (to indicate if it's an airport with regular commercial flights) + .setAttr("iata", sf.getString("iata")) + .setBufferPixels(8) + .setZoomRange(Math.min(minZoom, 15), 15); + + // Core Tilezen schema properties + if (!kindDetail.isEmpty()) + pointFeature.setAttr("kind_detail", kindDetail); + + OsmNames.setOsmNames(pointFeature, sf, 0); + + // Server sort features so client label collisions are pre-sorted + // NOTE: (nvkelso 20230627) This could also include other params like the name + pointFeature.setSortKey(minZoom * 1000); + + // Even with the categorical zoom bucketing above, we end up with too dense a point feature spread in downtown + // areas, so cull the labels which wouldn't label at earlier zooms than the max_zoom of 15 + pointFeature.setPointLabelGridSizeAndLimit(14, 8, 1); } } From 5dcb54886084d5e296a1a3facaaacd591f61790c Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Sat, 27 Dec 2025 12:26:59 -0800 Subject: [PATCH 19/33] De-duped some final logic to clarify identical point/polygon POI behavior --- .../com/protomaps/basemap/layers/Pois.java | 70 ++++++------------- 1 file changed, 20 insertions(+), 50 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index bfac12875..8bcb5a798 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -405,19 +405,20 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { if (zoomMatches.isEmpty()) return; + // Output feature and its basic values to assign + FeatureCollector.Feature pointFeature = null; String kind = getString(sf2, kindMatches, "kind", "undefined"); String kindDetail = getString(sf2, kindMatches, "kindDetail", "undefined"); Integer minZoom = getInteger(sf2, zoomMatches, "minZoom", 99); - long qrank = 0; - + // QRank may override minZoom entirely String wikidata = sf.getString("wikidata"); - if (wikidata != null) { - qrank = qrankDb.get(wikidata); - } + long qrank = (wikidata != null) ? qrankDb.get(wikidata) : 0; + var qrankedZoom = QrankDb.assignZoom(qrankGrading, kind, qrank); // try first for polygon -> point representations if (sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null) { + // Emphasize large international airports earlier // Because the area grading resets the earlier dispensation if (kind.equals("aerodrome")) { @@ -453,53 +454,21 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { } } - var rankedZoom = QrankDb.assignZoom(qrankGrading, kind, qrank); - if (rankedZoom.isPresent()) - minZoom = rankedZoom.get(); - - var polyLabelPosition = features.pointOnSurface(this.name()) - // all POIs should receive their IDs at all zooms - // (there is no merging of POIs like with lines and polygons in other layers) - .setId(FeatureId.create(sf)) - // Core Tilezen schema properties - .setAttr("kind", kind) - // While other layers don't need min_zoom, POIs do for more predictable client-side label collisions - // 512 px zooms versus 256 px logical zooms - .setAttr("min_zoom", minZoom + 1) - // + pointFeature = features.pointOnSurface(this.name()) // DEBUG //.setAttr("area_debug", wayArea) - // - // Core OSM tags for different kinds of places - // Special airport only tag (to indicate if it's an airport with regular commercial flights) - .setAttr("iata", sf.getString("iata")) - .setAttr("elevation", sf.getString("ele")) - // Extra OSM tags for certain kinds of places - // These are duplicate of what's in the kind_detail tag - .setBufferPixels(8) - .setZoomRange(Math.min(15, minZoom), 15); - - // Core Tilezen schema properties - if (!kindDetail.isEmpty()) { - polyLabelPosition.setAttr("kind_detail", kindDetail); - } - - OsmNames.setOsmNames(polyLabelPosition, sf, 0); - - // Server sort features so client label collisions are pre-sorted - // NOTE: (nvkelso 20230627) This could also include other params like the name - polyLabelPosition.setSortKey(minZoom * 1000); - - // Even with the categorical zoom bucketing above, we end up with too dense a point feature spread in downtown - // areas, so cull the labels which wouldn't label at earlier zooms than the max_zoom of 15 - polyLabelPosition.setPointLabelGridSizeAndLimit(14, 8, 1); + .setAttr("elevation", sf.getString("ele")); } else if (sf.isPoint()) { - var rankedZoom = QrankDb.assignZoom(qrankGrading, kind, qrank); - if (rankedZoom.isPresent()) - minZoom = rankedZoom.get(); + pointFeature = features.point(this.name()); + } - var pointFeature = features.point(this.name()) + if (pointFeature != null) { + // Override minZoom with QRank entirely + if (qrankedZoom.isPresent()) + minZoom = qrankedZoom.get(); + + pointFeature // all POIs should receive their IDs at all zooms // (there is no merging of POIs like with lines and polygons in other layers) .setId(FeatureId.create(sf)) @@ -508,11 +477,12 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { // While other layers don't need min_zoom, POIs do for more predictable client-side label collisions // 512 px zooms versus 256 px logical zooms .setAttr("min_zoom", minZoom + 1) + // + .setBufferPixels(8) + .setZoomRange(Math.min(minZoom, 15), 15) // Core OSM tags for different kinds of places // Special airport only tag (to indicate if it's an airport with regular commercial flights) - .setAttr("iata", sf.getString("iata")) - .setBufferPixels(8) - .setZoomRange(Math.min(minZoom, 15), 15); + .setAttr("iata", sf.getString("iata")); // Core Tilezen schema properties if (!kindDetail.isEmpty()) From 3a569b64bcbfdffba8819f1f211583335645c939 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Sat, 27 Dec 2025 12:42:02 -0800 Subject: [PATCH 20/33] Renamed and organized zoom rules for legibility --- .../com/protomaps/basemap/layers/Pois.java | 190 +++++++++--------- 1 file changed, 100 insertions(+), 90 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index 8bcb5a798..3533fc44f 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -48,9 +48,9 @@ public Pois(QrankDb qrankDb) { public static final String LAYER_NAME = "pois"; // Internal tags used to reference calculated values between matchers - private static final String KIND_ATTR = "protomaps-basemaps:kind"; - private static final String WAYAREA_ATTR = "protomaps-basemaps:wayArea"; - private static final String HEIGHT_ATTR = "protomaps-basemaps:height"; + private static final String KIND = "protomaps-basemaps:kind"; + private static final String WAYAREA = "protomaps-basemaps:wayArea"; + private static final String HEIGHT = "protomaps-basemaps:height"; private static final String HAS_NAMED_POLYGON = "protomaps-basemaps:hasNamedPolygon"; private static final Expression WITH_OPERATOR_USFS = with("operator", "United States Forest Service", @@ -154,17 +154,19 @@ public Pois(QrankDb qrankDb) { )).index(); + // Shorthand expressions to save space below + private static final Expression with_named_polygon = with(HAS_NAMED_POLYGON); private static final Expression with_s_c_named_poly = - Expression.and(with_named_polygon, with(KIND_ATTR, "cemetery", "school")); + Expression.and(with_named_polygon, with(KIND, "cemetery", "school")); private static final Expression with_n_p_named_poly = - Expression.and(with_named_polygon, with(KIND_ATTR, "national_park")); + Expression.and(with_named_polygon, with(KIND, "national_park")); private static final Expression with_c_u_named_poly = - Expression.and(with_named_polygon, with(KIND_ATTR, "college", "university")); + Expression.and(with_named_polygon, with(KIND, "college", "university")); private static final Expression with_b_g_named_poly = Expression.and(with_named_polygon, - with(KIND_ATTR, "forest", "park", "protected_area", "nature_reserve", "village_green")); + with(KIND, "forest", "park", "protected_area", "nature_reserve", "village_green")); private static final Expression with_etc_named_poly = Expression.and(with_named_polygon, - with(KIND_ATTR, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo")); + with(KIND, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo")); private static final MultiExpression.Index> zoomsIndex = MultiExpression.ofOrdered(List.of( @@ -190,24 +192,35 @@ public Pois(QrankDb qrankDb) { use("minZoom", 15) ), - // Fine-tune lots of specific categories + // Promote important point categories to earlier zooms - rule(with(KIND_ATTR, "national_park"), use("minZoom", 11)), - rule(with("natural", "peak"), use("minZoom", 13)), - rule(with("highway", "bus_stop"), use("minZoom", 17)), - rule(with("tourism", "attraction", "camp_site", "hotel"), use("minZoom", 15)), - rule(with("shop", "grocery", "supermarket"), use("minZoom", 14)), - rule(with("leisure", "golf_course", "marina", "stadium"), use("minZoom", 13)), - rule(with("leisure", "park"), use("minZoom", 14)), // Lots of pocket parks and NODE parks, show those later than rest of leisure - rule(with("landuse", "cemetery"), use("minZoom", 14)), - rule(with("amenity", "cafe"), use("minZoom", 15)), - rule(with("amenity", "school"), use("minZoom", 15)), - rule(with("amenity", "library", "post_office", "townhall"), use("minZoom", 13)), - rule(with("amenity", "hospital"), use("minZoom", 12)), - rule(with("amenity", "university", "college"), use("minZoom", 14)), // One would think University should be earlier, but there are lots of dinky node only places, so if the university has a large area, it'll naturally improve its zoom in another section... - rule(with("aeroway", "aerodrome"), use("minZoom", 13)), - rule(with("aeroway", "aerodrome"), with(KIND_ATTR, "aerodrome"), with("iata"), use("minZoom", 11)), // Emphasize large international airports earlier + rule( + withPoint(), + Expression.or( + with("amenity", "university", "college"), // One would think University should be earlier, but there are lots of dinky node only places, so if the university has a large area, it'll naturally improve its zoom in another section... + with("landuse", "cemetery"), + with("leisure", "park"), // Lots of pocket parks and NODE parks, show those later than rest of leisure + with("shop", "grocery", "supermarket") + ), + use("minZoom", 14) + ), + rule( + withPoint(), + Expression.or( + with("aeroway", "aerodrome"), + with("amenity", "library", "post_office", "townhall"), + with("leisure", "golf_course", "marina", "stadium"), + with("natural", "peak") + ), + use("minZoom", 13) + ), + rule(withPoint(), with("amenity", "hospital"), use("minZoom", 12)), + rule(withPoint(), with(KIND, "national_park"), use("minZoom", 11)), + rule(withPoint(), with("aeroway", "aerodrome"), with(KIND, "aerodrome"), with("iata"), use("minZoom", 11)), // Emphasize large international airports earlier + // Demote some unimportant point categories to very late zooms + + rule(with("highway", "bus_stop"), use("minZoom", 17)), rule( withPoint(), Expression.or( @@ -229,7 +242,7 @@ public Pois(QrankDb qrankDb) { use("minZoom", 16) ), - // Some features should only be visible at very late zooms when they don't have a name + // Demote some unnamed point categories to very late zooms rule( withPoint(), @@ -251,101 +264,98 @@ public Pois(QrankDb qrankDb) { // Size-graded polygons, generic at first then per-kind adjustments - rule(with_named_polygon, withinRange(WAYAREA_ATTR, 10, 500), use("minZoom", 14)), - rule(with_named_polygon, withinRange(WAYAREA_ATTR, 500, 2000), use("minZoom", 13)), - rule(with_named_polygon, withinRange(WAYAREA_ATTR, 2000, 1e4), use("minZoom", 12)), - rule(with_named_polygon, withinRange(WAYAREA_ATTR, 1e4), use("minZoom", 11)), + rule(with_named_polygon, withinRange(WAYAREA, 10, 500), use("minZoom", 14)), + rule(with_named_polygon, withinRange(WAYAREA, 500, 2000), use("minZoom", 13)), + rule(with_named_polygon, withinRange(WAYAREA, 2000, 1e4), use("minZoom", 12)), + rule(with_named_polygon, withinRange(WAYAREA, 1e4), use("minZoom", 11)), - rule(with_named_polygon, with(KIND_ATTR, "playground"), use("minZoom", 17)), - rule(with_named_polygon, with(KIND_ATTR, "allotments"), withinRange(WAYAREA_ATTR, 0, 10), use("minZoom", 16)), - rule(with_named_polygon, with(KIND_ATTR, "allotments"), withinRange(WAYAREA_ATTR, 10), use("minZoom", 15)), + rule(with_named_polygon, with(KIND, "playground"), use("minZoom", 17)), + rule(with_named_polygon, with(KIND, "allotments"), withinRange(WAYAREA, 0, 10), use("minZoom", 16)), + rule(with_named_polygon, with(KIND, "allotments"), withinRange(WAYAREA, 10), use("minZoom", 15)), // Height-graded polygons, generic at first then per-kind adjustments - // Small but tall features should show up early as they have regional prominance. + // Small but tall features should show up early as they have regional prominence. // Height measured in meters - rule(with_named_polygon, withinRange(WAYAREA_ATTR, 10, 2000), withinRange(HEIGHT_ATTR, 10, 20), use("minZoom", 13)), - rule(with_named_polygon, withinRange(WAYAREA_ATTR, 10, 2000), withinRange(HEIGHT_ATTR, 20, 100), - use("minZoom", 12)), - rule(with_named_polygon, withinRange(WAYAREA_ATTR, 10, 2000), withinRange(HEIGHT_ATTR, 100), - use("minZoom", 11)), + rule(with_named_polygon, withinRange(WAYAREA, 10, 2000), withinRange(HEIGHT, 10, 20), use("minZoom", 13)), + rule(with_named_polygon, withinRange(WAYAREA, 10, 2000), withinRange(HEIGHT, 20, 100), use("minZoom", 12)), + rule(with_named_polygon, withinRange(WAYAREA, 10, 2000), withinRange(HEIGHT, 100), use("minZoom", 11)), // Clamp certain kind values so medium tall buildings don't crowd downtown areas // NOTE: (nvkelso 20230623) Apply label grid to early zooms of POIs layer // NOTE: (nvkelso 20230624) Turn this into an allowlist instead of a blocklist rule( with_named_polygon, - with(KIND_ATTR, "hotel", "hostel", "parking", "bank", "place_of_worship", "jewelry", "yes", "restaurant", + with(KIND, "hotel", "hostel", "parking", "bank", "place_of_worship", "jewelry", "yes", "restaurant", "coworking_space", "clothes", "art", "school"), - withinRange(WAYAREA_ATTR, 10, 2000), - withinRange(HEIGHT_ATTR, 20, 100), + withinRange(WAYAREA, 10, 2000), + withinRange(HEIGHT, 20, 100), use("minZoom", 13) ), // Discount tall self storage buildings - rule(with_named_polygon, with(KIND_ATTR, "storage_rental"), withinRange(WAYAREA_ATTR, 10, 2000), - use("minZoom", 14)), + rule(with_named_polygon, with(KIND, "storage_rental"), withinRange(WAYAREA, 10, 2000), use("minZoom", 14)), // Discount tall university buildings, require a related university landuse AOI - rule(with_named_polygon, with(KIND_ATTR, "university"), withinRange(WAYAREA_ATTR, 10, 2000), use("minZoom", 13)), + rule(with_named_polygon, with(KIND, "university"), withinRange(WAYAREA, 10, 2000), use("minZoom", 13)), // Schools & Cemeteries - rule(with_s_c_named_poly, withinRange(WAYAREA_ATTR, 0, 10), use("minZoom", 16)), - rule(with_s_c_named_poly, withinRange(WAYAREA_ATTR, 10, 100), use("minZoom", 15)), - rule(with_s_c_named_poly, withinRange(WAYAREA_ATTR, 100, 1000), use("minZoom", 14)), - rule(with_s_c_named_poly, withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), - rule(with_s_c_named_poly, withinRange(WAYAREA_ATTR, 5000), use("minZoom", 12)), + rule(with_s_c_named_poly, withinRange(WAYAREA, 0, 10), use("minZoom", 16)), + rule(with_s_c_named_poly, withinRange(WAYAREA, 10, 100), use("minZoom", 15)), + rule(with_s_c_named_poly, withinRange(WAYAREA, 100, 1000), use("minZoom", 14)), + rule(with_s_c_named_poly, withinRange(WAYAREA, 1000, 5000), use("minZoom", 13)), + rule(with_s_c_named_poly, withinRange(WAYAREA, 5000), use("minZoom", 12)), // National parks - rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 0, 250), use("minZoom", 17)), - rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 250, 1000), use("minZoom", 14)), - rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), - rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 5000, 2e4), use("minZoom", 12)), - rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 2e4, 1e5), use("minZoom", 11)), - rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 1e5, 2.5e5), use("minZoom", 10)), - rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 2.5e5, 2e6), use("minZoom", 9)), - rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 2e6, 1e7), use("minZoom", 8)), - rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 1e7, 2.5e7), use("minZoom", 7)), - rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 2.5e7, 3e8), use("minZoom", 6)), - rule(with_n_p_named_poly, withinRange(WAYAREA_ATTR, 3e8), use("minZoom", 5)), + rule(with_n_p_named_poly, withinRange(WAYAREA, 0, 250), use("minZoom", 17)), + rule(with_n_p_named_poly, withinRange(WAYAREA, 250, 1000), use("minZoom", 14)), + rule(with_n_p_named_poly, withinRange(WAYAREA, 1000, 5000), use("minZoom", 13)), + rule(with_n_p_named_poly, withinRange(WAYAREA, 5000, 2e4), use("minZoom", 12)), + rule(with_n_p_named_poly, withinRange(WAYAREA, 2e4, 1e5), use("minZoom", 11)), + rule(with_n_p_named_poly, withinRange(WAYAREA, 1e5, 2.5e5), use("minZoom", 10)), + rule(with_n_p_named_poly, withinRange(WAYAREA, 2.5e5, 2e6), use("minZoom", 9)), + rule(with_n_p_named_poly, withinRange(WAYAREA, 2e6, 1e7), use("minZoom", 8)), + rule(with_n_p_named_poly, withinRange(WAYAREA, 1e7, 2.5e7), use("minZoom", 7)), + rule(with_n_p_named_poly, withinRange(WAYAREA, 2.5e7, 3e8), use("minZoom", 6)), + rule(with_n_p_named_poly, withinRange(WAYAREA, 3e8), use("minZoom", 5)), // College and university polygons - rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 0, 5000), use("minZoom", 15)), - rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 5000, 2e4), use("minZoom", 14)), - rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 2e4, 5e4), use("minZoom", 13)), - rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 5e4, 1e5), use("minZoom", 12)), - rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 1e5, 1.5e5), use("minZoom", 11)), - rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 1.5e5, 2.5e5), use("minZoom", 10)), - rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 2.5e5, 5e6), use("minZoom", 9)), - rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 5e6, 2e7), use("minZoom", 8)), - rule(with_c_u_named_poly, withinRange(WAYAREA_ATTR, 2e7), use("minZoom", 7)), + rule(with_c_u_named_poly, withinRange(WAYAREA, 0, 5000), use("minZoom", 15)), + rule(with_c_u_named_poly, withinRange(WAYAREA, 5000, 2e4), use("minZoom", 14)), + rule(with_c_u_named_poly, withinRange(WAYAREA, 2e4, 5e4), use("minZoom", 13)), + rule(with_c_u_named_poly, withinRange(WAYAREA, 5e4, 1e5), use("minZoom", 12)), + rule(with_c_u_named_poly, withinRange(WAYAREA, 1e5, 1.5e5), use("minZoom", 11)), + rule(with_c_u_named_poly, withinRange(WAYAREA, 1.5e5, 2.5e5), use("minZoom", 10)), + rule(with_c_u_named_poly, withinRange(WAYAREA, 2.5e5, 5e6), use("minZoom", 9)), + rule(with_c_u_named_poly, withinRange(WAYAREA, 5e6, 2e7), use("minZoom", 8)), + rule(with_c_u_named_poly, withinRange(WAYAREA, 2e7), use("minZoom", 7)), rule(with_c_u_named_poly, with("name", "Academy of Art University"), use("minZoom", 14)), // Hack for weird San Francisco university // Big green polygons - rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 0, 1), use("minZoom", 17)), - rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 1, 10), use("minZoom", 16)), - rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 10, 250), use("minZoom", 15)), - rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 250, 1000), use("minZoom", 14)), - rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), - rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 5000, 1.5e4), use("minZoom", 12)), - rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 1.5e4, 2.5e5), use("minZoom", 11)), - rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 2.5e5, 1e6), use("minZoom", 10)), - rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 1e6, 4e6), use("minZoom", 9)), - rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 4e6, 1e7), use("minZoom", 8)), - rule(with_b_g_named_poly, withinRange(WAYAREA_ATTR, 1e7), use("minZoom", 7)), + rule(with_b_g_named_poly, withinRange(WAYAREA, 0, 1), use("minZoom", 17)), + rule(with_b_g_named_poly, withinRange(WAYAREA, 1, 10), use("minZoom", 16)), + rule(with_b_g_named_poly, withinRange(WAYAREA, 10, 250), use("minZoom", 15)), + rule(with_b_g_named_poly, withinRange(WAYAREA, 250, 1000), use("minZoom", 14)), + rule(with_b_g_named_poly, withinRange(WAYAREA, 1000, 5000), use("minZoom", 13)), + rule(with_b_g_named_poly, withinRange(WAYAREA, 5000, 1.5e4), use("minZoom", 12)), + rule(with_b_g_named_poly, withinRange(WAYAREA, 1.5e4, 2.5e5), use("minZoom", 11)), + rule(with_b_g_named_poly, withinRange(WAYAREA, 2.5e5, 1e6), use("minZoom", 10)), + rule(with_b_g_named_poly, withinRange(WAYAREA, 1e6, 4e6), use("minZoom", 9)), + rule(with_b_g_named_poly, withinRange(WAYAREA, 4e6, 1e7), use("minZoom", 8)), + rule(with_b_g_named_poly, withinRange(WAYAREA, 1e7), use("minZoom", 7)), // Remaining grab-bag of scaled kinds - rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 250, 1000), use("minZoom", 14)), - rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 1000, 5000), use("minZoom", 13)), - rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 5000, 2e4), use("minZoom", 12)), - rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 2e4, 1e5), use("minZoom", 11)), - rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 1e5, 2.5e5), use("minZoom", 10)), - rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 2.5e5, 5e6), use("minZoom", 9)), - rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 5e6, 2e7), use("minZoom", 8)), - rule(with_etc_named_poly, withinRange(WAYAREA_ATTR, 2e7), use("minZoom", 7)) + rule(with_etc_named_poly, withinRange(WAYAREA, 250, 1000), use("minZoom", 14)), + rule(with_etc_named_poly, withinRange(WAYAREA, 1000, 5000), use("minZoom", 13)), + rule(with_etc_named_poly, withinRange(WAYAREA, 5000, 2e4), use("minZoom", 12)), + rule(with_etc_named_poly, withinRange(WAYAREA, 2e4, 1e5), use("minZoom", 11)), + rule(with_etc_named_poly, withinRange(WAYAREA, 1e5, 2.5e5), use("minZoom", 10)), + rule(with_etc_named_poly, withinRange(WAYAREA, 2.5e5, 5e6), use("minZoom", 9)), + rule(with_etc_named_poly, withinRange(WAYAREA, 5e6, 2e7), use("minZoom", 8)), + rule(with_etc_named_poly, withinRange(WAYAREA, 2e7), use("minZoom", 7)) )).index(); @@ -381,9 +391,9 @@ public Matcher.SourceFeatureWithComputedTags computeExtraTags(SourceFeature sf, Map computedTags; if (hasNamedPolygon) { - computedTags = Map.of(KIND_ATTR, kind, WAYAREA_ATTR, wayArea, HEIGHT_ATTR, height, HAS_NAMED_POLYGON, true); + computedTags = Map.of(KIND, kind, WAYAREA, wayArea, HEIGHT, height, HAS_NAMED_POLYGON, true); } else { - computedTags = Map.of(KIND_ATTR, kind, WAYAREA_ATTR, wayArea, HEIGHT_ATTR, height); + computedTags = Map.of(KIND, kind, WAYAREA, wayArea, HEIGHT, height); } return new Matcher.SourceFeatureWithComputedTags(sf, computedTags); From 2525a5c9a2c32dc69be1034cc3ffe4c65a846a59 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Sat, 27 Dec 2025 12:54:16 -0800 Subject: [PATCH 21/33] Tweak, tweak, tweak --- .../com/protomaps/basemap/layers/Pois.java | 79 +++++++++---------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index 3533fc44f..e713dd891 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -400,7 +400,7 @@ public Matcher.SourceFeatureWithComputedTags computeExtraTags(SourceFeature sf, } public void processOsm(SourceFeature sf, FeatureCollector features) { - // We only do points and polygons for POI labels + // We only do POI display for points and polygons if (!sf.isPoint() && !sf.canBePolygon()) return; @@ -416,7 +416,7 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { return; // Output feature and its basic values to assign - FeatureCollector.Feature pointFeature = null; + FeatureCollector.Feature outputFeature = null; String kind = getString(sf2, kindMatches, "kind", "undefined"); String kindDetail = getString(sf2, kindMatches, "kindDetail", "undefined"); Integer minZoom = getInteger(sf2, zoomMatches, "minZoom", 99); @@ -426,7 +426,6 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { long qrank = (wikidata != null) ? qrankDb.get(wikidata) : 0; var qrankedZoom = QrankDb.assignZoom(qrankGrading, kind, qrank); - // try first for polygon -> point representations if (sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null) { // Emphasize large international airports earlier @@ -464,50 +463,50 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { } } - pointFeature = features.pointOnSurface(this.name()) - // DEBUG - //.setAttr("area_debug", wayArea) + outputFeature = features.pointOnSurface(this.name()) + //.setAttr("area_debug", wayArea) // DEBUG .setAttr("elevation", sf.getString("ele")); } else if (sf.isPoint()) { - pointFeature = features.point(this.name()); + outputFeature = features.point(this.name()); + } else { + return; } - if (pointFeature != null) { - // Override minZoom with QRank entirely - if (qrankedZoom.isPresent()) - minZoom = qrankedZoom.get(); - - pointFeature - // all POIs should receive their IDs at all zooms - // (there is no merging of POIs like with lines and polygons in other layers) - .setId(FeatureId.create(sf)) - // Core Tilezen schema properties - .setAttr("kind", kind) - // While other layers don't need min_zoom, POIs do for more predictable client-side label collisions - // 512 px zooms versus 256 px logical zooms - .setAttr("min_zoom", minZoom + 1) - // - .setBufferPixels(8) - .setZoomRange(Math.min(minZoom, 15), 15) - // Core OSM tags for different kinds of places - // Special airport only tag (to indicate if it's an airport with regular commercial flights) - .setAttr("iata", sf.getString("iata")); + // Override minZoom with QRank entirely + if (qrankedZoom.isPresent()) + minZoom = qrankedZoom.get(); + // Populate final outputFeature attributes + outputFeature + // all POIs should receive their IDs at all zooms + // (there is no merging of POIs like with lines and polygons in other layers) + .setId(FeatureId.create(sf)) // Core Tilezen schema properties - if (!kindDetail.isEmpty()) - pointFeature.setAttr("kind_detail", kindDetail); - - OsmNames.setOsmNames(pointFeature, sf, 0); - - // Server sort features so client label collisions are pre-sorted - // NOTE: (nvkelso 20230627) This could also include other params like the name - pointFeature.setSortKey(minZoom * 1000); - - // Even with the categorical zoom bucketing above, we end up with too dense a point feature spread in downtown - // areas, so cull the labels which wouldn't label at earlier zooms than the max_zoom of 15 - pointFeature.setPointLabelGridSizeAndLimit(14, 8, 1); - } + .setAttr("kind", kind) + // While other layers don't need min_zoom, POIs do for more predictable client-side label collisions + // 512 px zooms versus 256 px logical zooms + .setAttr("min_zoom", minZoom + 1) + // + .setBufferPixels(8) + .setZoomRange(Math.min(minZoom, 15), 15) + // Core OSM tags for different kinds of places + // Special airport only tag (to indicate if it's an airport with regular commercial flights) + .setAttr("iata", sf.getString("iata")); + + // Core Tilezen schema properties + if (!kindDetail.isEmpty()) + outputFeature.setAttr("kind_detail", kindDetail); + + OsmNames.setOsmNames(outputFeature, sf, 0); + + // Server sort features so client label collisions are pre-sorted + // NOTE: (nvkelso 20230627) This could also include other params like the name + outputFeature.setSortKey(minZoom * 1000); + + // Even with the categorical zoom bucketing above, we end up with too dense a point feature spread in downtown + // areas, so cull the labels which wouldn't label at earlier zooms than the max_zoom of 15 + outputFeature.setPointLabelGridSizeAndLimit(14, 8, 1); } @Override From 1982412b6b917b12565c1a685b3eb0a7edfbab63 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Sat, 27 Dec 2025 13:06:18 -0800 Subject: [PATCH 22/33] More tweak, tweak, tweak --- .../com/protomaps/basemap/layers/Pois.java | 98 ++++++++++--------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index e713dd891..2761e0d91 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -400,8 +400,8 @@ public Matcher.SourceFeatureWithComputedTags computeExtraTags(SourceFeature sf, } public void processOsm(SourceFeature sf, FeatureCollector features) { - // We only do POI display for points and polygons - if (!sf.isPoint() && !sf.canBePolygon()) + // We only do POI display for points and named polygons + if (!(sf.isPoint() || sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null)) return; // Map the Protomaps "kind" classification to incoming tags @@ -409,74 +409,80 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { if (kindMatches.isEmpty()) return; - // Calculate dimensions and create a wrapper with computed tags - var sf2 = computeExtraTags(sf, getString(sf, kindMatches, "kind", "undefined")); - var zoomMatches = zoomsIndex.getMatches(sf2); - if (zoomMatches.isEmpty()) - return; - // Output feature and its basic values to assign - FeatureCollector.Feature outputFeature = null; - String kind = getString(sf2, kindMatches, "kind", "undefined"); - String kindDetail = getString(sf2, kindMatches, "kindDetail", "undefined"); - Integer minZoom = getInteger(sf2, zoomMatches, "minZoom", 99); + FeatureCollector.Feature outputFeature; + String kind = getString(sf, kindMatches, "kind", "undefined"); + String kindDetail = getString(sf, kindMatches, "kindDetail", "undefined"); + Integer minZoom; // QRank may override minZoom entirely String wikidata = sf.getString("wikidata"); long qrank = (wikidata != null) ? qrankDb.get(wikidata) : 0; var qrankedZoom = QrankDb.assignZoom(qrankGrading, kind, qrank); - if (sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null) { - - // Emphasize large international airports earlier - // Because the area grading resets the earlier dispensation - if (kind.equals("aerodrome")) { - if (sf.hasTag("iata")) { - // prioritize international airports over regional airports - minZoom -= 2; - - // but don't show international airports tooooo early - if (minZoom < 10) { - minZoom = 10; - } - } else { - // and show other airports only once their polygon begins to be visible - if (minZoom < 12) { - minZoom = 12; + if (qrankedZoom.isPresent()) { + // Set minZoom from QRank + minZoom = qrankedZoom.get(); + } else { + // Calculate minZoom using zoomsIndex + var sf2 = computeExtraTags(sf, getString(sf, kindMatches, "kind", "undefined")); + var zoomMatches = zoomsIndex.getMatches(sf2); + if (zoomMatches.isEmpty()) + return; + + // Initial minZoom + minZoom = getInteger(sf2, zoomMatches, "minZoom", 99); + + // Adjusted minZoom + if (sf.canBePolygon()) { + // Emphasize large international airports earlier + // Because the area grading resets the earlier dispensation + if (kind.equals("aerodrome")) { + if (sf.hasTag("iata")) { + // prioritize international airports over regional airports + minZoom -= 2; + + // but don't show international airports tooooo early + if (minZoom < 10) { + minZoom = 10; + } + } else { + // and show other airports only once their polygon begins to be visible + if (minZoom < 12) { + minZoom = 12; + } } } - } - // Discount wilderness areas within US national forests and parks - if (kind.equals("nature_reserve") && sf.getString("name").contains("Wilderness")) { - minZoom += 1; - } + // Discount wilderness areas within US national forests and parks + if (kind.equals("nature_reserve") && sf.getString("name").contains("Wilderness")) { + minZoom += 1; + } - // very long text names should only be shown at later zooms - if (minZoom < 14) { - var nameLength = sf.getString("name").length(); + // very long text names should only be shown at later zooms + if (minZoom < 14) { + var nameLength = sf.getString("name").length(); - if (nameLength > 45) { - minZoom += 2; - } else if (nameLength > 30) { - minZoom += 1; + if (nameLength > 45) { + minZoom += 2; + } else if (nameLength > 30) { + minZoom += 1; + } } } + } + // Assign outputFeature + if (sf.canBePolygon()) { outputFeature = features.pointOnSurface(this.name()) //.setAttr("area_debug", wayArea) // DEBUG .setAttr("elevation", sf.getString("ele")); - } else if (sf.isPoint()) { outputFeature = features.point(this.name()); } else { return; } - // Override minZoom with QRank entirely - if (qrankedZoom.isPresent()) - minZoom = qrankedZoom.get(); - // Populate final outputFeature attributes outputFeature // all POIs should receive their IDs at all zooms From f0ea92a16b37044935ac0da01d4ea2708bcffff0 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Sat, 27 Dec 2025 13:17:10 -0800 Subject: [PATCH 23/33] Carved zoomsIndex into point-and-polygon-specific MultiExpressions --- .../com/protomaps/basemap/layers/Pois.java | 278 +++++++++--------- 1 file changed, 138 insertions(+), 140 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index 2761e0d91..52e52ca40 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -7,7 +7,6 @@ import static com.protomaps.basemap.feature.Matcher.rule; import static com.protomaps.basemap.feature.Matcher.use; import static com.protomaps.basemap.feature.Matcher.with; -import static com.protomaps.basemap.feature.Matcher.withPoint; import static com.protomaps.basemap.feature.Matcher.withinRange; import static com.protomaps.basemap.feature.Matcher.without; @@ -156,46 +155,23 @@ public Pois(QrankDb qrankDb) { // Shorthand expressions to save space below - private static final Expression with_named_polygon = with(HAS_NAMED_POLYGON); - private static final Expression with_s_c_named_poly = - Expression.and(with_named_polygon, with(KIND, "cemetery", "school")); - private static final Expression with_n_p_named_poly = - Expression.and(with_named_polygon, with(KIND, "national_park")); - private static final Expression with_c_u_named_poly = - Expression.and(with_named_polygon, with(KIND, "college", "university")); - private static final Expression with_b_g_named_poly = Expression.and(with_named_polygon, - with(KIND, "forest", "park", "protected_area", "nature_reserve", "village_green")); - private static final Expression with_etc_named_poly = Expression.and(with_named_polygon, - with(KIND, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo")); - - private static final MultiExpression.Index> zoomsIndex = MultiExpression.ofOrdered(List.of( - - // Everything with a point or a valid tag is zoom=15 at first - rule( - Expression.or( - withPoint(), - with("aeroway", "aerodrome"), - with("amenity"), - with("attraction"), - with("boundary", "national_park", "protected_area"), - with("craft"), - with("highway", "bus_stop"), - with("historic"), - with("landuse", "cemetery", "recreation_ground", "winter_sports", "quarry", "park", "forest", "military", - "village_green", "allotments"), - with("leisure"), - with("natural", "beach", "peak"), - with("railway", "station"), - with("shop"), - Expression.and(with("tourism"), without("historic", "district")) - ), - use("minZoom", 15) - ), + private static final Expression with_s_c = with(KIND, "cemetery", "school"); + private static final Expression with_n_p = with(KIND, "national_park"); + private static final Expression with_c_u = with(KIND, "college", "university"); + private static final Expression with_b_g = + with(KIND, "forest", "park", "protected_area", "nature_reserve", "village_green"); + private static final Expression with_etc = + with(KIND, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"); + + private static final MultiExpression.Index> pointZoomsIndex = MultiExpression.ofOrdered(List.of( + + // Every point is zoom=15 at first + + rule(use("minZoom", 15)), // Promote important point categories to earlier zooms rule( - withPoint(), Expression.or( with("amenity", "university", "college"), // One would think University should be earlier, but there are lots of dinky node only places, so if the university has a large area, it'll naturally improve its zoom in another section... with("landuse", "cemetery"), @@ -205,7 +181,6 @@ public Pois(QrankDb qrankDb) { use("minZoom", 14) ), rule( - withPoint(), Expression.or( with("aeroway", "aerodrome"), with("amenity", "library", "post_office", "townhall"), @@ -214,15 +189,14 @@ public Pois(QrankDb qrankDb) { ), use("minZoom", 13) ), - rule(withPoint(), with("amenity", "hospital"), use("minZoom", 12)), - rule(withPoint(), with(KIND, "national_park"), use("minZoom", 11)), - rule(withPoint(), with("aeroway", "aerodrome"), with(KIND, "aerodrome"), with("iata"), use("minZoom", 11)), // Emphasize large international airports earlier + rule(with("amenity", "hospital"), use("minZoom", 12)), + rule(with(KIND, "national_park"), use("minZoom", 11)), + rule(with("aeroway", "aerodrome"), with(KIND, "aerodrome"), with("iata"), use("minZoom", 11)), // Emphasize large international airports earlier // Demote some unimportant point categories to very late zooms rule(with("highway", "bus_stop"), use("minZoom", 17)), rule( - withPoint(), Expression.or( with("amenity", "clinic", "dentist", "doctors", "social_facility", "baby_hatch", "childcare", "car_sharing", "bureau_de_change", "emergency_phone", "karaoke", "karaoke_box", "money_transfer", "car_wash", @@ -245,7 +219,6 @@ public Pois(QrankDb qrankDb) { // Demote some unnamed point categories to very late zooms rule( - withPoint(), without("name"), Expression.or( with("amenity", "atm", "bbq", "bench", "bicycle_parking", @@ -260,104 +233,129 @@ public Pois(QrankDb qrankDb) { with("tourism", "alpine_hut", "information", "picnic_site", "viewpoint", "wilderness_hut") ), use("minZoom", 16) - ), - - // Size-graded polygons, generic at first then per-kind adjustments - - rule(with_named_polygon, withinRange(WAYAREA, 10, 500), use("minZoom", 14)), - rule(with_named_polygon, withinRange(WAYAREA, 500, 2000), use("minZoom", 13)), - rule(with_named_polygon, withinRange(WAYAREA, 2000, 1e4), use("minZoom", 12)), - rule(with_named_polygon, withinRange(WAYAREA, 1e4), use("minZoom", 11)), - - rule(with_named_polygon, with(KIND, "playground"), use("minZoom", 17)), - rule(with_named_polygon, with(KIND, "allotments"), withinRange(WAYAREA, 0, 10), use("minZoom", 16)), - rule(with_named_polygon, with(KIND, "allotments"), withinRange(WAYAREA, 10), use("minZoom", 15)), - - // Height-graded polygons, generic at first then per-kind adjustments - // Small but tall features should show up early as they have regional prominence. - // Height measured in meters + ) - rule(with_named_polygon, withinRange(WAYAREA, 10, 2000), withinRange(HEIGHT, 10, 20), use("minZoom", 13)), - rule(with_named_polygon, withinRange(WAYAREA, 10, 2000), withinRange(HEIGHT, 20, 100), use("minZoom", 12)), - rule(with_named_polygon, withinRange(WAYAREA, 10, 2000), withinRange(HEIGHT, 100), use("minZoom", 11)), - - // Clamp certain kind values so medium tall buildings don't crowd downtown areas - // NOTE: (nvkelso 20230623) Apply label grid to early zooms of POIs layer - // NOTE: (nvkelso 20230624) Turn this into an allowlist instead of a blocklist - rule( - with_named_polygon, - with(KIND, "hotel", "hostel", "parking", "bank", "place_of_worship", "jewelry", "yes", "restaurant", - "coworking_space", "clothes", "art", "school"), - withinRange(WAYAREA, 10, 2000), - withinRange(HEIGHT, 20, 100), - use("minZoom", 13) - ), - // Discount tall self storage buildings - rule(with_named_polygon, with(KIND, "storage_rental"), withinRange(WAYAREA, 10, 2000), use("minZoom", 14)), - // Discount tall university buildings, require a related university landuse AOI - rule(with_named_polygon, with(KIND, "university"), withinRange(WAYAREA, 10, 2000), use("minZoom", 13)), - - // Schools & Cemeteries - - rule(with_s_c_named_poly, withinRange(WAYAREA, 0, 10), use("minZoom", 16)), - rule(with_s_c_named_poly, withinRange(WAYAREA, 10, 100), use("minZoom", 15)), - rule(with_s_c_named_poly, withinRange(WAYAREA, 100, 1000), use("minZoom", 14)), - rule(with_s_c_named_poly, withinRange(WAYAREA, 1000, 5000), use("minZoom", 13)), - rule(with_s_c_named_poly, withinRange(WAYAREA, 5000), use("minZoom", 12)), - - // National parks + )).index(); - rule(with_n_p_named_poly, withinRange(WAYAREA, 0, 250), use("minZoom", 17)), - rule(with_n_p_named_poly, withinRange(WAYAREA, 250, 1000), use("minZoom", 14)), - rule(with_n_p_named_poly, withinRange(WAYAREA, 1000, 5000), use("minZoom", 13)), - rule(with_n_p_named_poly, withinRange(WAYAREA, 5000, 2e4), use("minZoom", 12)), - rule(with_n_p_named_poly, withinRange(WAYAREA, 2e4, 1e5), use("minZoom", 11)), - rule(with_n_p_named_poly, withinRange(WAYAREA, 1e5, 2.5e5), use("minZoom", 10)), - rule(with_n_p_named_poly, withinRange(WAYAREA, 2.5e5, 2e6), use("minZoom", 9)), - rule(with_n_p_named_poly, withinRange(WAYAREA, 2e6, 1e7), use("minZoom", 8)), - rule(with_n_p_named_poly, withinRange(WAYAREA, 1e7, 2.5e7), use("minZoom", 7)), - rule(with_n_p_named_poly, withinRange(WAYAREA, 2.5e7, 3e8), use("minZoom", 6)), - rule(with_n_p_named_poly, withinRange(WAYAREA, 3e8), use("minZoom", 5)), - - // College and university polygons - - rule(with_c_u_named_poly, withinRange(WAYAREA, 0, 5000), use("minZoom", 15)), - rule(with_c_u_named_poly, withinRange(WAYAREA, 5000, 2e4), use("minZoom", 14)), - rule(with_c_u_named_poly, withinRange(WAYAREA, 2e4, 5e4), use("minZoom", 13)), - rule(with_c_u_named_poly, withinRange(WAYAREA, 5e4, 1e5), use("minZoom", 12)), - rule(with_c_u_named_poly, withinRange(WAYAREA, 1e5, 1.5e5), use("minZoom", 11)), - rule(with_c_u_named_poly, withinRange(WAYAREA, 1.5e5, 2.5e5), use("minZoom", 10)), - rule(with_c_u_named_poly, withinRange(WAYAREA, 2.5e5, 5e6), use("minZoom", 9)), - rule(with_c_u_named_poly, withinRange(WAYAREA, 5e6, 2e7), use("minZoom", 8)), - rule(with_c_u_named_poly, withinRange(WAYAREA, 2e7), use("minZoom", 7)), - rule(with_c_u_named_poly, with("name", "Academy of Art University"), use("minZoom", 14)), // Hack for weird San Francisco university - - // Big green polygons - - rule(with_b_g_named_poly, withinRange(WAYAREA, 0, 1), use("minZoom", 17)), - rule(with_b_g_named_poly, withinRange(WAYAREA, 1, 10), use("minZoom", 16)), - rule(with_b_g_named_poly, withinRange(WAYAREA, 10, 250), use("minZoom", 15)), - rule(with_b_g_named_poly, withinRange(WAYAREA, 250, 1000), use("minZoom", 14)), - rule(with_b_g_named_poly, withinRange(WAYAREA, 1000, 5000), use("minZoom", 13)), - rule(with_b_g_named_poly, withinRange(WAYAREA, 5000, 1.5e4), use("minZoom", 12)), - rule(with_b_g_named_poly, withinRange(WAYAREA, 1.5e4, 2.5e5), use("minZoom", 11)), - rule(with_b_g_named_poly, withinRange(WAYAREA, 2.5e5, 1e6), use("minZoom", 10)), - rule(with_b_g_named_poly, withinRange(WAYAREA, 1e6, 4e6), use("minZoom", 9)), - rule(with_b_g_named_poly, withinRange(WAYAREA, 4e6, 1e7), use("minZoom", 8)), - rule(with_b_g_named_poly, withinRange(WAYAREA, 1e7), use("minZoom", 7)), - - // Remaining grab-bag of scaled kinds - - rule(with_etc_named_poly, withinRange(WAYAREA, 250, 1000), use("minZoom", 14)), - rule(with_etc_named_poly, withinRange(WAYAREA, 1000, 5000), use("minZoom", 13)), - rule(with_etc_named_poly, withinRange(WAYAREA, 5000, 2e4), use("minZoom", 12)), - rule(with_etc_named_poly, withinRange(WAYAREA, 2e4, 1e5), use("minZoom", 11)), - rule(with_etc_named_poly, withinRange(WAYAREA, 1e5, 2.5e5), use("minZoom", 10)), - rule(with_etc_named_poly, withinRange(WAYAREA, 2.5e5, 5e6), use("minZoom", 9)), - rule(with_etc_named_poly, withinRange(WAYAREA, 5e6, 2e7), use("minZoom", 8)), - rule(with_etc_named_poly, withinRange(WAYAREA, 2e7), use("minZoom", 7)) + private static final MultiExpression.Index> namedPolygonZoomsIndex = + MultiExpression.ofOrdered(List.of( + + // Every named polygon with a valid tag is zoom=15 at first + rule( + Expression.or( + with("aeroway", "aerodrome"), + with("amenity"), + with("attraction"), + with("boundary", "national_park", "protected_area"), + with("craft"), + with("highway", "bus_stop"), + with("historic"), + with("landuse", "cemetery", "recreation_ground", "winter_sports", "quarry", "park", "forest", "military", + "village_green", "allotments"), + with("leisure"), + with("natural", "beach", "peak"), + with("railway", "station"), + with("shop"), + Expression.and(with("tourism"), without("historic", "district")) + ), + use("minZoom", 15) + ), - )).index(); + // Size-graded polygons, generic at first then per-kind adjustments + + rule(withinRange(WAYAREA, 10, 500), use("minZoom", 14)), + rule(withinRange(WAYAREA, 500, 2000), use("minZoom", 13)), + rule(withinRange(WAYAREA, 2000, 1e4), use("minZoom", 12)), + rule(withinRange(WAYAREA, 1e4), use("minZoom", 11)), + + rule(with(KIND, "playground"), use("minZoom", 17)), + rule(with(KIND, "allotments"), withinRange(WAYAREA, 0, 10), use("minZoom", 16)), + rule(with(KIND, "allotments"), withinRange(WAYAREA, 10), use("minZoom", 15)), + + // Height-graded polygons, generic at first then per-kind adjustments + // Small but tall features should show up early as they have regional prominence. + // Height measured in meters + + rule(withinRange(WAYAREA, 10, 2000), withinRange(HEIGHT, 10, 20), use("minZoom", 13)), + rule(withinRange(WAYAREA, 10, 2000), withinRange(HEIGHT, 20, 100), use("minZoom", 12)), + rule(withinRange(WAYAREA, 10, 2000), withinRange(HEIGHT, 100), use("minZoom", 11)), + + // Clamp certain kind values so medium tall buildings don't crowd downtown areas + // NOTE: (nvkelso 20230623) Apply label grid to early zooms of POIs layer + // NOTE: (nvkelso 20230624) Turn this into an allowlist instead of a blocklist + rule( + with(KIND, "hotel", "hostel", "parking", "bank", "place_of_worship", "jewelry", "yes", "restaurant", + "coworking_space", "clothes", "art", "school"), + withinRange(WAYAREA, 10, 2000), + withinRange(HEIGHT, 20, 100), + use("minZoom", 13) + ), + // Discount tall self storage buildings + rule(with(KIND, "storage_rental"), withinRange(WAYAREA, 10, 2000), use("minZoom", 14)), + // Discount tall university buildings, require a related university landuse AOI + rule(with(KIND, "university"), withinRange(WAYAREA, 10, 2000), use("minZoom", 13)), + + // Schools & Cemeteries + + rule(with_s_c, withinRange(WAYAREA, 0, 10), use("minZoom", 16)), + rule(with_s_c, withinRange(WAYAREA, 10, 100), use("minZoom", 15)), + rule(with_s_c, withinRange(WAYAREA, 100, 1000), use("minZoom", 14)), + rule(with_s_c, withinRange(WAYAREA, 1000, 5000), use("minZoom", 13)), + rule(with_s_c, withinRange(WAYAREA, 5000), use("minZoom", 12)), + + // National parks + + rule(with_n_p, withinRange(WAYAREA, 0, 250), use("minZoom", 17)), + rule(with_n_p, withinRange(WAYAREA, 250, 1000), use("minZoom", 14)), + rule(with_n_p, withinRange(WAYAREA, 1000, 5000), use("minZoom", 13)), + rule(with_n_p, withinRange(WAYAREA, 5000, 2e4), use("minZoom", 12)), + rule(with_n_p, withinRange(WAYAREA, 2e4, 1e5), use("minZoom", 11)), + rule(with_n_p, withinRange(WAYAREA, 1e5, 2.5e5), use("minZoom", 10)), + rule(with_n_p, withinRange(WAYAREA, 2.5e5, 2e6), use("minZoom", 9)), + rule(with_n_p, withinRange(WAYAREA, 2e6, 1e7), use("minZoom", 8)), + rule(with_n_p, withinRange(WAYAREA, 1e7, 2.5e7), use("minZoom", 7)), + rule(with_n_p, withinRange(WAYAREA, 2.5e7, 3e8), use("minZoom", 6)), + rule(with_n_p, withinRange(WAYAREA, 3e8), use("minZoom", 5)), + + // College and university polygons + + rule(with_c_u, withinRange(WAYAREA, 0, 5000), use("minZoom", 15)), + rule(with_c_u, withinRange(WAYAREA, 5000, 2e4), use("minZoom", 14)), + rule(with_c_u, withinRange(WAYAREA, 2e4, 5e4), use("minZoom", 13)), + rule(with_c_u, withinRange(WAYAREA, 5e4, 1e5), use("minZoom", 12)), + rule(with_c_u, withinRange(WAYAREA, 1e5, 1.5e5), use("minZoom", 11)), + rule(with_c_u, withinRange(WAYAREA, 1.5e5, 2.5e5), use("minZoom", 10)), + rule(with_c_u, withinRange(WAYAREA, 2.5e5, 5e6), use("minZoom", 9)), + rule(with_c_u, withinRange(WAYAREA, 5e6, 2e7), use("minZoom", 8)), + rule(with_c_u, withinRange(WAYAREA, 2e7), use("minZoom", 7)), + rule(with_c_u, with("name", "Academy of Art University"), use("minZoom", 14)), // Hack for weird San Francisco university + + // Big green polygons + + rule(with_b_g, withinRange(WAYAREA, 0, 1), use("minZoom", 17)), + rule(with_b_g, withinRange(WAYAREA, 1, 10), use("minZoom", 16)), + rule(with_b_g, withinRange(WAYAREA, 10, 250), use("minZoom", 15)), + rule(with_b_g, withinRange(WAYAREA, 250, 1000), use("minZoom", 14)), + rule(with_b_g, withinRange(WAYAREA, 1000, 5000), use("minZoom", 13)), + rule(with_b_g, withinRange(WAYAREA, 5000, 1.5e4), use("minZoom", 12)), + rule(with_b_g, withinRange(WAYAREA, 1.5e4, 2.5e5), use("minZoom", 11)), + rule(with_b_g, withinRange(WAYAREA, 2.5e5, 1e6), use("minZoom", 10)), + rule(with_b_g, withinRange(WAYAREA, 1e6, 4e6), use("minZoom", 9)), + rule(with_b_g, withinRange(WAYAREA, 4e6, 1e7), use("minZoom", 8)), + rule(with_b_g, withinRange(WAYAREA, 1e7), use("minZoom", 7)), + + // Remaining grab-bag of scaled kinds + + rule(with_etc, withinRange(WAYAREA, 250, 1000), use("minZoom", 14)), + rule(with_etc, withinRange(WAYAREA, 1000, 5000), use("minZoom", 13)), + rule(with_etc, withinRange(WAYAREA, 5000, 2e4), use("minZoom", 12)), + rule(with_etc, withinRange(WAYAREA, 2e4, 1e5), use("minZoom", 11)), + rule(with_etc, withinRange(WAYAREA, 1e5, 2.5e5), use("minZoom", 10)), + rule(with_etc, withinRange(WAYAREA, 2.5e5, 5e6), use("minZoom", 9)), + rule(with_etc, withinRange(WAYAREA, 5e6, 2e7), use("minZoom", 8)), + rule(with_etc, withinRange(WAYAREA, 2e7), use("minZoom", 7)) + + )).index(); @Override public String name() { @@ -424,9 +422,9 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { // Set minZoom from QRank minZoom = qrankedZoom.get(); } else { - // Calculate minZoom using zoomsIndex + // Calculate minZoom using zooms indexes var sf2 = computeExtraTags(sf, getString(sf, kindMatches, "kind", "undefined")); - var zoomMatches = zoomsIndex.getMatches(sf2); + var zoomMatches = sf.canBePolygon() ? namedPolygonZoomsIndex.getMatches(sf2) : pointZoomsIndex.getMatches(sf2); if (zoomMatches.isEmpty()) return; From 479a5e42d237cc8d5c19ef849b44238624a6f93a Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Sat, 27 Dec 2025 13:37:50 -0800 Subject: [PATCH 24/33] Fixed overly-broad point matches on irrelevant tags --- .../com/protomaps/basemap/layers/Pois.java | 83 ++++++++++--------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index 52e52ca40..063cb2254 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -51,6 +51,7 @@ public Pois(QrankDb qrankDb) { private static final String WAYAREA = "protomaps-basemaps:wayArea"; private static final String HEIGHT = "protomaps-basemaps:height"; private static final String HAS_NAMED_POLYGON = "protomaps-basemaps:hasNamedPolygon"; + private static final String UNDEFINED = "protomaps-basemaps:undefined"; private static final Expression WITH_OPERATOR_USFS = with("operator", "United States Forest Service", "US Forest Service", "U.S. Forest Service", "USDA Forest Service", "United States Department of Agriculture", @@ -58,8 +59,29 @@ public Pois(QrankDb qrankDb) { private static final MultiExpression.Index> kindsIndex = MultiExpression.ofOrdered(List.of( - // Everything is "other"/"" at first - rule(use("kind", "other"), use("kindDetail", "")), + // Everything is undefined at first + rule(use("kind", UNDEFINED), use("kindDetail", UNDEFINED)), + + // An initial set of tags we like + rule( + Expression.or( + with("aeroway", "aerodrome"), + with("amenity"), + with("attraction"), + with("boundary", "national_park", "protected_area"), + with("craft"), + with("highway", "bus_stop"), + with("historic"), + with("landuse", "cemetery", "recreation_ground", "winter_sports", "quarry", "park", "forest", "military", + "village_green", "allotments"), + with("leisure"), + with("natural", "beach", "peak"), + with("railway", "station"), + with("shop"), + Expression.and(with("tourism"), without("historic", "district")) + ), + use("kind", "other") + ), // Boundary is most generic, so place early else we lose out // on nature_reserve detail versus all the protected_area @@ -153,20 +175,9 @@ public Pois(QrankDb qrankDb) { )).index(); - // Shorthand expressions to save space below - - private static final Expression with_s_c = with(KIND, "cemetery", "school"); - private static final Expression with_n_p = with(KIND, "national_park"); - private static final Expression with_c_u = with(KIND, "college", "university"); - private static final Expression with_b_g = - with(KIND, "forest", "park", "protected_area", "nature_reserve", "village_green"); - private static final Expression with_etc = - with(KIND, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"); - private static final MultiExpression.Index> pointZoomsIndex = MultiExpression.ofOrdered(List.of( // Every point is zoom=15 at first - rule(use("minZoom", 15)), // Promote important point categories to earlier zooms @@ -237,29 +248,21 @@ public Pois(QrankDb qrankDb) { )).index(); + // Shorthand expressions to save space below + + private static final Expression with_s_c = with(KIND, "cemetery", "school"); + private static final Expression with_n_p = with(KIND, "national_park"); + private static final Expression with_c_u = with(KIND, "college", "university"); + private static final Expression with_b_g = + with(KIND, "forest", "park", "protected_area", "nature_reserve", "village_green"); + private static final Expression with_etc = + with(KIND, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"); + private static final MultiExpression.Index> namedPolygonZoomsIndex = MultiExpression.ofOrdered(List.of( - // Every named polygon with a valid tag is zoom=15 at first - rule( - Expression.or( - with("aeroway", "aerodrome"), - with("amenity"), - with("attraction"), - with("boundary", "national_park", "protected_area"), - with("craft"), - with("highway", "bus_stop"), - with("historic"), - with("landuse", "cemetery", "recreation_ground", "winter_sports", "quarry", "park", "forest", "military", - "village_green", "allotments"), - with("leisure"), - with("natural", "beach", "peak"), - with("railway", "station"), - with("shop"), - Expression.and(with("tourism"), without("historic", "district")) - ), - use("minZoom", 15) - ), + // Every named polygon is zoom=15 at first + rule(use("minZoom", 15)), // Size-graded polygons, generic at first then per-kind adjustments @@ -404,15 +407,17 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { // Map the Protomaps "kind" classification to incoming tags var kindMatches = kindsIndex.getMatches(sf); - if (kindMatches.isEmpty()) - return; // Output feature and its basic values to assign FeatureCollector.Feature outputFeature; - String kind = getString(sf, kindMatches, "kind", "undefined"); - String kindDetail = getString(sf, kindMatches, "kindDetail", "undefined"); + String kind = getString(sf, kindMatches, "kind", UNDEFINED); + String kindDetail = getString(sf, kindMatches, "kindDetail", UNDEFINED); Integer minZoom; + // Quickly eliminate any features with non-matching tags + if (kind == UNDEFINED) + return; + // QRank may override minZoom entirely String wikidata = sf.getString("wikidata"); long qrank = (wikidata != null) ? qrankDb.get(wikidata) : 0; @@ -423,7 +428,7 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { minZoom = qrankedZoom.get(); } else { // Calculate minZoom using zooms indexes - var sf2 = computeExtraTags(sf, getString(sf, kindMatches, "kind", "undefined")); + var sf2 = computeExtraTags(sf, getString(sf, kindMatches, "kind", UNDEFINED)); var zoomMatches = sf.canBePolygon() ? namedPolygonZoomsIndex.getMatches(sf2) : pointZoomsIndex.getMatches(sf2); if (zoomMatches.isEmpty()) return; @@ -499,7 +504,7 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { .setAttr("iata", sf.getString("iata")); // Core Tilezen schema properties - if (!kindDetail.isEmpty()) + if (kindDetail != UNDEFINED) outputFeature.setAttr("kind_detail", kindDetail); OsmNames.setOsmNames(outputFeature, sf, 0); From 415e587690894d34a2732dcbc2b1ddceb69f069d Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Sat, 27 Dec 2025 13:56:25 -0800 Subject: [PATCH 25/33] Switched to constants for KIND and MINZOOM strings --- .../com/protomaps/basemap/layers/Pois.java | 210 +++++++++--------- 1 file changed, 106 insertions(+), 104 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index 063cb2254..73a52e3a6 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -48,6 +48,8 @@ public Pois(QrankDb qrankDb) { // Internal tags used to reference calculated values between matchers private static final String KIND = "protomaps-basemaps:kind"; + private static final String KIND_DETAIL = "protomaps-basemaps:kindDetail"; + private static final String MINZOOM = "protomaps-basemaps:minZoom"; private static final String WAYAREA = "protomaps-basemaps:wayArea"; private static final String HEIGHT = "protomaps-basemaps:height"; private static final String HAS_NAMED_POLYGON = "protomaps-basemaps:hasNamedPolygon"; @@ -60,7 +62,7 @@ public Pois(QrankDb qrankDb) { private static final MultiExpression.Index> kindsIndex = MultiExpression.ofOrdered(List.of( // Everything is undefined at first - rule(use("kind", UNDEFINED), use("kindDetail", UNDEFINED)), + rule(use(KIND, UNDEFINED), use(KIND_DETAIL, UNDEFINED)), // An initial set of tags we like rule( @@ -80,27 +82,27 @@ public Pois(QrankDb qrankDb) { with("shop"), Expression.and(with("tourism"), without("historic", "district")) ), - use("kind", "other") + use(KIND, "other") ), // Boundary is most generic, so place early else we lose out // on nature_reserve detail versus all the protected_area - rule(with("boundary"), use("kind", fromTag("boundary"))), + rule(with("boundary"), use(KIND, fromTag("boundary"))), // More specific kinds - rule(with("historic"), without("historic", "yes"), use("kind", fromTag("historic"))), - rule(with("tourism"), use("kind", fromTag("tourism"))), - rule(with("shop"), use("kind", fromTag("shop"))), - rule(with("highway"), use("kind", fromTag("highway"))), - rule(with("railway"), use("kind", fromTag("railway"))), - rule(with("natural"), use("kind", fromTag("natural"))), - rule(with("leisure"), use("kind", fromTag("leisure"))), - rule(with("landuse"), use("kind", fromTag("landuse"))), - rule(with("aeroway"), use("kind", fromTag("aeroway"))), - rule(with("craft"), use("kind", fromTag("craft"))), - rule(with("attraction"), use("kind", fromTag("attraction"))), - rule(with("amenity"), use("kind", fromTag("amenity"))), + rule(with("historic"), without("historic", "yes"), use(KIND, fromTag("historic"))), + rule(with("tourism"), use(KIND, fromTag("tourism"))), + rule(with("shop"), use(KIND, fromTag("shop"))), + rule(with("highway"), use(KIND, fromTag("highway"))), + rule(with("railway"), use(KIND, fromTag("railway"))), + rule(with("natural"), use(KIND, fromTag("natural"))), + rule(with("leisure"), use(KIND, fromTag("leisure"))), + rule(with("landuse"), use(KIND, fromTag("landuse"))), + rule(with("aeroway"), use(KIND, fromTag("aeroway"))), + rule(with("craft"), use(KIND, fromTag("craft"))), + rule(with("attraction"), use(KIND, fromTag("attraction"))), + rule(with("amenity"), use(KIND, fromTag("amenity"))), // National forests @@ -119,12 +121,12 @@ public Pois(QrankDb qrankDb) { WITH_OPERATOR_USFS ) ), - use("kind", "forest") + use(KIND, "forest") ), // National parks - rule(with("boundary", "national_park"), use("kind", "park")), + rule(with("boundary", "national_park"), use(KIND, "park")), rule( with("boundary", "national_park"), Expression.not(WITH_OPERATOR_USFS), @@ -141,44 +143,44 @@ public Pois(QrankDb qrankDb) { with("designation", "national_park"), with("protection_title", "National Park") ), - use("kind", "national_park") + use(KIND, "national_park") ), // Remaining things - rule(with("natural", "peak"), use("kind", fromTag("natural"))), - rule(with("highway", "bus_stop"), use("kind", fromTag("highway"))), - rule(with("tourism", "attraction", "camp_site", "hotel"), use("kind", fromTag("tourism"))), - rule(with("shop", "grocery", "supermarket"), use("kind", fromTag("shop"))), - rule(with("leisure", "golf_course", "marina", "stadium", "park"), use("kind", fromTag("leisure"))), + rule(with("natural", "peak"), use(KIND, fromTag("natural"))), + rule(with("highway", "bus_stop"), use(KIND, fromTag("highway"))), + rule(with("tourism", "attraction", "camp_site", "hotel"), use(KIND, fromTag("tourism"))), + rule(with("shop", "grocery", "supermarket"), use(KIND, fromTag("shop"))), + rule(with("leisure", "golf_course", "marina", "stadium", "park"), use(KIND, fromTag("leisure"))), - rule(with("landuse", "military"), use("kind", "military")), + rule(with("landuse", "military"), use(KIND, "military")), rule( with("landuse", "military"), with("military", "naval_base", "airfield"), - use("kind", fromTag("military")) + use(KIND, fromTag("military")) ), - rule(with("landuse", "cemetery"), use("kind", fromTag("landuse"))), + rule(with("landuse", "cemetery"), use(KIND, fromTag("landuse"))), rule( with("aeroway", "aerodrome"), - use("kind", "aerodrome"), - use("kindDetail", fromTag("aerodrome")) + use(KIND, "aerodrome"), + use(KIND_DETAIL, fromTag("aerodrome")) ), // Additional details for certain classes of POI - rule(with("sport"), use("kindDetail", fromTag("sport"))), - rule(with("religion"), use("kindDetail", fromTag("religion"))), - rule(with("cuisine"), use("kindDetail", fromTag("cuisine"))) + rule(with("sport"), use(KIND_DETAIL, fromTag("sport"))), + rule(with("religion"), use(KIND_DETAIL, fromTag("religion"))), + rule(with("cuisine"), use(KIND_DETAIL, fromTag("cuisine"))) )).index(); private static final MultiExpression.Index> pointZoomsIndex = MultiExpression.ofOrdered(List.of( // Every point is zoom=15 at first - rule(use("minZoom", 15)), + rule(use(MINZOOM, 15)), // Promote important point categories to earlier zooms @@ -189,7 +191,7 @@ public Pois(QrankDb qrankDb) { with("leisure", "park"), // Lots of pocket parks and NODE parks, show those later than rest of leisure with("shop", "grocery", "supermarket") ), - use("minZoom", 14) + use(MINZOOM, 14) ), rule( Expression.or( @@ -198,15 +200,15 @@ public Pois(QrankDb qrankDb) { with("leisure", "golf_course", "marina", "stadium"), with("natural", "peak") ), - use("minZoom", 13) + use(MINZOOM, 13) ), - rule(with("amenity", "hospital"), use("minZoom", 12)), - rule(with(KIND, "national_park"), use("minZoom", 11)), - rule(with("aeroway", "aerodrome"), with(KIND, "aerodrome"), with("iata"), use("minZoom", 11)), // Emphasize large international airports earlier + rule(with("amenity", "hospital"), use(MINZOOM, 12)), + rule(with(KIND, "national_park"), use(MINZOOM, 11)), + rule(with("aeroway", "aerodrome"), with(KIND, "aerodrome"), with("iata"), use(MINZOOM, 11)), // Emphasize large international airports earlier // Demote some unimportant point categories to very late zooms - rule(with("highway", "bus_stop"), use("minZoom", 17)), + rule(with("highway", "bus_stop"), use(MINZOOM, 17)), rule( Expression.or( with("amenity", "clinic", "dentist", "doctors", "social_facility", "baby_hatch", "childcare", @@ -224,7 +226,7 @@ public Pois(QrankDb qrankDb) { with("tourism", "artwork", "hanami", "trail_riding_station", "bed_and_breakfast", "chalet", "guest_house", "hostel") ), - use("minZoom", 16) + use(MINZOOM, 16) ), // Demote some unnamed point categories to very late zooms @@ -243,7 +245,7 @@ public Pois(QrankDb qrankDb) { with("leisure", "dog_park", "firepit", "fishing", "pitch", "playground", "slipway", "swimming_area"), with("tourism", "alpine_hut", "information", "picnic_site", "viewpoint", "wilderness_hut") ), - use("minZoom", 16) + use(MINZOOM, 16) ) )).index(); @@ -262,26 +264,26 @@ public Pois(QrankDb qrankDb) { MultiExpression.ofOrdered(List.of( // Every named polygon is zoom=15 at first - rule(use("minZoom", 15)), + rule(use(MINZOOM, 15)), // Size-graded polygons, generic at first then per-kind adjustments - rule(withinRange(WAYAREA, 10, 500), use("minZoom", 14)), - rule(withinRange(WAYAREA, 500, 2000), use("minZoom", 13)), - rule(withinRange(WAYAREA, 2000, 1e4), use("minZoom", 12)), - rule(withinRange(WAYAREA, 1e4), use("minZoom", 11)), + rule(withinRange(WAYAREA, 10, 500), use(MINZOOM, 14)), + rule(withinRange(WAYAREA, 500, 2000), use(MINZOOM, 13)), + rule(withinRange(WAYAREA, 2000, 1e4), use(MINZOOM, 12)), + rule(withinRange(WAYAREA, 1e4), use(MINZOOM, 11)), - rule(with(KIND, "playground"), use("minZoom", 17)), - rule(with(KIND, "allotments"), withinRange(WAYAREA, 0, 10), use("minZoom", 16)), - rule(with(KIND, "allotments"), withinRange(WAYAREA, 10), use("minZoom", 15)), + rule(with(KIND, "playground"), use(MINZOOM, 17)), + rule(with(KIND, "allotments"), withinRange(WAYAREA, 0, 10), use(MINZOOM, 16)), + rule(with(KIND, "allotments"), withinRange(WAYAREA, 10), use(MINZOOM, 15)), // Height-graded polygons, generic at first then per-kind adjustments // Small but tall features should show up early as they have regional prominence. // Height measured in meters - rule(withinRange(WAYAREA, 10, 2000), withinRange(HEIGHT, 10, 20), use("minZoom", 13)), - rule(withinRange(WAYAREA, 10, 2000), withinRange(HEIGHT, 20, 100), use("minZoom", 12)), - rule(withinRange(WAYAREA, 10, 2000), withinRange(HEIGHT, 100), use("minZoom", 11)), + rule(withinRange(WAYAREA, 10, 2000), withinRange(HEIGHT, 10, 20), use(MINZOOM, 13)), + rule(withinRange(WAYAREA, 10, 2000), withinRange(HEIGHT, 20, 100), use(MINZOOM, 12)), + rule(withinRange(WAYAREA, 10, 2000), withinRange(HEIGHT, 100), use(MINZOOM, 11)), // Clamp certain kind values so medium tall buildings don't crowd downtown areas // NOTE: (nvkelso 20230623) Apply label grid to early zooms of POIs layer @@ -291,72 +293,72 @@ public Pois(QrankDb qrankDb) { "coworking_space", "clothes", "art", "school"), withinRange(WAYAREA, 10, 2000), withinRange(HEIGHT, 20, 100), - use("minZoom", 13) + use(MINZOOM, 13) ), // Discount tall self storage buildings - rule(with(KIND, "storage_rental"), withinRange(WAYAREA, 10, 2000), use("minZoom", 14)), + rule(with(KIND, "storage_rental"), withinRange(WAYAREA, 10, 2000), use(MINZOOM, 14)), // Discount tall university buildings, require a related university landuse AOI - rule(with(KIND, "university"), withinRange(WAYAREA, 10, 2000), use("minZoom", 13)), + rule(with(KIND, "university"), withinRange(WAYAREA, 10, 2000), use(MINZOOM, 13)), // Schools & Cemeteries - rule(with_s_c, withinRange(WAYAREA, 0, 10), use("minZoom", 16)), - rule(with_s_c, withinRange(WAYAREA, 10, 100), use("minZoom", 15)), - rule(with_s_c, withinRange(WAYAREA, 100, 1000), use("minZoom", 14)), - rule(with_s_c, withinRange(WAYAREA, 1000, 5000), use("minZoom", 13)), - rule(with_s_c, withinRange(WAYAREA, 5000), use("minZoom", 12)), + rule(with_s_c, withinRange(WAYAREA, 0, 10), use(MINZOOM, 16)), + rule(with_s_c, withinRange(WAYAREA, 10, 100), use(MINZOOM, 15)), + rule(with_s_c, withinRange(WAYAREA, 100, 1000), use(MINZOOM, 14)), + rule(with_s_c, withinRange(WAYAREA, 1000, 5000), use(MINZOOM, 13)), + rule(with_s_c, withinRange(WAYAREA, 5000), use(MINZOOM, 12)), // National parks - rule(with_n_p, withinRange(WAYAREA, 0, 250), use("minZoom", 17)), - rule(with_n_p, withinRange(WAYAREA, 250, 1000), use("minZoom", 14)), - rule(with_n_p, withinRange(WAYAREA, 1000, 5000), use("minZoom", 13)), - rule(with_n_p, withinRange(WAYAREA, 5000, 2e4), use("minZoom", 12)), - rule(with_n_p, withinRange(WAYAREA, 2e4, 1e5), use("minZoom", 11)), - rule(with_n_p, withinRange(WAYAREA, 1e5, 2.5e5), use("minZoom", 10)), - rule(with_n_p, withinRange(WAYAREA, 2.5e5, 2e6), use("minZoom", 9)), - rule(with_n_p, withinRange(WAYAREA, 2e6, 1e7), use("minZoom", 8)), - rule(with_n_p, withinRange(WAYAREA, 1e7, 2.5e7), use("minZoom", 7)), - rule(with_n_p, withinRange(WAYAREA, 2.5e7, 3e8), use("minZoom", 6)), - rule(with_n_p, withinRange(WAYAREA, 3e8), use("minZoom", 5)), + rule(with_n_p, withinRange(WAYAREA, 0, 250), use(MINZOOM, 17)), + rule(with_n_p, withinRange(WAYAREA, 250, 1000), use(MINZOOM, 14)), + rule(with_n_p, withinRange(WAYAREA, 1000, 5000), use(MINZOOM, 13)), + rule(with_n_p, withinRange(WAYAREA, 5000, 2e4), use(MINZOOM, 12)), + rule(with_n_p, withinRange(WAYAREA, 2e4, 1e5), use(MINZOOM, 11)), + rule(with_n_p, withinRange(WAYAREA, 1e5, 2.5e5), use(MINZOOM, 10)), + rule(with_n_p, withinRange(WAYAREA, 2.5e5, 2e6), use(MINZOOM, 9)), + rule(with_n_p, withinRange(WAYAREA, 2e6, 1e7), use(MINZOOM, 8)), + rule(with_n_p, withinRange(WAYAREA, 1e7, 2.5e7), use(MINZOOM, 7)), + rule(with_n_p, withinRange(WAYAREA, 2.5e7, 3e8), use(MINZOOM, 6)), + rule(with_n_p, withinRange(WAYAREA, 3e8), use(MINZOOM, 5)), // College and university polygons - rule(with_c_u, withinRange(WAYAREA, 0, 5000), use("minZoom", 15)), - rule(with_c_u, withinRange(WAYAREA, 5000, 2e4), use("minZoom", 14)), - rule(with_c_u, withinRange(WAYAREA, 2e4, 5e4), use("minZoom", 13)), - rule(with_c_u, withinRange(WAYAREA, 5e4, 1e5), use("minZoom", 12)), - rule(with_c_u, withinRange(WAYAREA, 1e5, 1.5e5), use("minZoom", 11)), - rule(with_c_u, withinRange(WAYAREA, 1.5e5, 2.5e5), use("minZoom", 10)), - rule(with_c_u, withinRange(WAYAREA, 2.5e5, 5e6), use("minZoom", 9)), - rule(with_c_u, withinRange(WAYAREA, 5e6, 2e7), use("minZoom", 8)), - rule(with_c_u, withinRange(WAYAREA, 2e7), use("minZoom", 7)), - rule(with_c_u, with("name", "Academy of Art University"), use("minZoom", 14)), // Hack for weird San Francisco university + rule(with_c_u, withinRange(WAYAREA, 0, 5000), use(MINZOOM, 15)), + rule(with_c_u, withinRange(WAYAREA, 5000, 2e4), use(MINZOOM, 14)), + rule(with_c_u, withinRange(WAYAREA, 2e4, 5e4), use(MINZOOM, 13)), + rule(with_c_u, withinRange(WAYAREA, 5e4, 1e5), use(MINZOOM, 12)), + rule(with_c_u, withinRange(WAYAREA, 1e5, 1.5e5), use(MINZOOM, 11)), + rule(with_c_u, withinRange(WAYAREA, 1.5e5, 2.5e5), use(MINZOOM, 10)), + rule(with_c_u, withinRange(WAYAREA, 2.5e5, 5e6), use(MINZOOM, 9)), + rule(with_c_u, withinRange(WAYAREA, 5e6, 2e7), use(MINZOOM, 8)), + rule(with_c_u, withinRange(WAYAREA, 2e7), use(MINZOOM, 7)), + rule(with_c_u, with("name", "Academy of Art University"), use(MINZOOM, 14)), // Hack for weird San Francisco university // Big green polygons - rule(with_b_g, withinRange(WAYAREA, 0, 1), use("minZoom", 17)), - rule(with_b_g, withinRange(WAYAREA, 1, 10), use("minZoom", 16)), - rule(with_b_g, withinRange(WAYAREA, 10, 250), use("minZoom", 15)), - rule(with_b_g, withinRange(WAYAREA, 250, 1000), use("minZoom", 14)), - rule(with_b_g, withinRange(WAYAREA, 1000, 5000), use("minZoom", 13)), - rule(with_b_g, withinRange(WAYAREA, 5000, 1.5e4), use("minZoom", 12)), - rule(with_b_g, withinRange(WAYAREA, 1.5e4, 2.5e5), use("minZoom", 11)), - rule(with_b_g, withinRange(WAYAREA, 2.5e5, 1e6), use("minZoom", 10)), - rule(with_b_g, withinRange(WAYAREA, 1e6, 4e6), use("minZoom", 9)), - rule(with_b_g, withinRange(WAYAREA, 4e6, 1e7), use("minZoom", 8)), - rule(with_b_g, withinRange(WAYAREA, 1e7), use("minZoom", 7)), + rule(with_b_g, withinRange(WAYAREA, 0, 1), use(MINZOOM, 17)), + rule(with_b_g, withinRange(WAYAREA, 1, 10), use(MINZOOM, 16)), + rule(with_b_g, withinRange(WAYAREA, 10, 250), use(MINZOOM, 15)), + rule(with_b_g, withinRange(WAYAREA, 250, 1000), use(MINZOOM, 14)), + rule(with_b_g, withinRange(WAYAREA, 1000, 5000), use(MINZOOM, 13)), + rule(with_b_g, withinRange(WAYAREA, 5000, 1.5e4), use(MINZOOM, 12)), + rule(with_b_g, withinRange(WAYAREA, 1.5e4, 2.5e5), use(MINZOOM, 11)), + rule(with_b_g, withinRange(WAYAREA, 2.5e5, 1e6), use(MINZOOM, 10)), + rule(with_b_g, withinRange(WAYAREA, 1e6, 4e6), use(MINZOOM, 9)), + rule(with_b_g, withinRange(WAYAREA, 4e6, 1e7), use(MINZOOM, 8)), + rule(with_b_g, withinRange(WAYAREA, 1e7), use(MINZOOM, 7)), // Remaining grab-bag of scaled kinds - rule(with_etc, withinRange(WAYAREA, 250, 1000), use("minZoom", 14)), - rule(with_etc, withinRange(WAYAREA, 1000, 5000), use("minZoom", 13)), - rule(with_etc, withinRange(WAYAREA, 5000, 2e4), use("minZoom", 12)), - rule(with_etc, withinRange(WAYAREA, 2e4, 1e5), use("minZoom", 11)), - rule(with_etc, withinRange(WAYAREA, 1e5, 2.5e5), use("minZoom", 10)), - rule(with_etc, withinRange(WAYAREA, 2.5e5, 5e6), use("minZoom", 9)), - rule(with_etc, withinRange(WAYAREA, 5e6, 2e7), use("minZoom", 8)), - rule(with_etc, withinRange(WAYAREA, 2e7), use("minZoom", 7)) + rule(with_etc, withinRange(WAYAREA, 250, 1000), use(MINZOOM, 14)), + rule(with_etc, withinRange(WAYAREA, 1000, 5000), use(MINZOOM, 13)), + rule(with_etc, withinRange(WAYAREA, 5000, 2e4), use(MINZOOM, 12)), + rule(with_etc, withinRange(WAYAREA, 2e4, 1e5), use(MINZOOM, 11)), + rule(with_etc, withinRange(WAYAREA, 1e5, 2.5e5), use(MINZOOM, 10)), + rule(with_etc, withinRange(WAYAREA, 2.5e5, 5e6), use(MINZOOM, 9)), + rule(with_etc, withinRange(WAYAREA, 5e6, 2e7), use(MINZOOM, 8)), + rule(with_etc, withinRange(WAYAREA, 2e7), use(MINZOOM, 7)) )).index(); @@ -405,13 +407,13 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { if (!(sf.isPoint() || sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null)) return; - // Map the Protomaps "kind" classification to incoming tags + // Map the Protomaps KIND classification to incoming tags var kindMatches = kindsIndex.getMatches(sf); // Output feature and its basic values to assign FeatureCollector.Feature outputFeature; - String kind = getString(sf, kindMatches, "kind", UNDEFINED); - String kindDetail = getString(sf, kindMatches, "kindDetail", UNDEFINED); + String kind = getString(sf, kindMatches, KIND, UNDEFINED); + String kindDetail = getString(sf, kindMatches, KIND_DETAIL, UNDEFINED); Integer minZoom; // Quickly eliminate any features with non-matching tags @@ -428,13 +430,13 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { minZoom = qrankedZoom.get(); } else { // Calculate minZoom using zooms indexes - var sf2 = computeExtraTags(sf, getString(sf, kindMatches, "kind", UNDEFINED)); + var sf2 = computeExtraTags(sf, getString(sf, kindMatches, KIND, UNDEFINED)); var zoomMatches = sf.canBePolygon() ? namedPolygonZoomsIndex.getMatches(sf2) : pointZoomsIndex.getMatches(sf2); if (zoomMatches.isEmpty()) return; // Initial minZoom - minZoom = getInteger(sf2, zoomMatches, "minZoom", 99); + minZoom = getInteger(sf2, zoomMatches, MINZOOM, 99); // Adjusted minZoom if (sf.canBePolygon()) { From d619032a6202cd0c0b650fff33548f5d2c7fe5d1 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Sat, 27 Dec 2025 14:07:32 -0800 Subject: [PATCH 26/33] Cleanup --- .../main/java/com/protomaps/basemap/feature/Matcher.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java b/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java index c81fd7d8a..36646ef5b 100644 --- a/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java +++ b/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java @@ -222,12 +222,6 @@ public boolean evaluate(com.onthegomap.planetiler.reader.WithTags input, List= lowerBound && (upperBound == null || value < upperBound); } - - @Override - public String generateJavaCode() { - return "withinRange(" + com.onthegomap.planetiler.util.Format.quote(tagName) + ", " + lowerBound + "L, " + - (upperBound == null ? "null" : upperBound + "L") + ")"; - } } public static Expression withPoint() { From 2dfde30b72eca0d6d609b1218a97b6140770e894 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Sat, 27 Dec 2025 17:48:17 -0800 Subject: [PATCH 27/33] Condensed some long booleans --- .../com/protomaps/basemap/layers/Pois.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index 73a52e3a6..642523998 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -371,13 +371,16 @@ public String name() { private static final double WORLD_AREA_FOR_70_SQUARE_METERS = Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70)) / 256d, 2); + private Boolean isNamedPolygon(SourceFeature sf) { + return sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null; + } + public Matcher.SourceFeatureWithComputedTags computeExtraTags(SourceFeature sf, String kind) { Double wayArea = 0.0; Double height = 0.0; - Boolean hasNamedPolygon = false; + Boolean hasNamedPolygon = isNamedPolygon(sf); - if (sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null) { - hasNamedPolygon = true; + if (hasNamedPolygon) { try { wayArea = sf.worldGeometry().getEnvelopeInternal().getArea() / WORLD_AREA_FOR_70_SQUARE_METERS; } catch (GeometryException e) { @@ -403,8 +406,10 @@ public Matcher.SourceFeatureWithComputedTags computeExtraTags(SourceFeature sf, } public void processOsm(SourceFeature sf, FeatureCollector features) { + Boolean hasNamedPolygon = isNamedPolygon(sf); + // We only do POI display for points and named polygons - if (!(sf.isPoint() || sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null)) + if (!sf.isPoint() && !hasNamedPolygon) return; // Map the Protomaps KIND classification to incoming tags @@ -431,7 +436,7 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { } else { // Calculate minZoom using zooms indexes var sf2 = computeExtraTags(sf, getString(sf, kindMatches, KIND, UNDEFINED)); - var zoomMatches = sf.canBePolygon() ? namedPolygonZoomsIndex.getMatches(sf2) : pointZoomsIndex.getMatches(sf2); + var zoomMatches = hasNamedPolygon ? namedPolygonZoomsIndex.getMatches(sf2) : pointZoomsIndex.getMatches(sf2); if (zoomMatches.isEmpty()) return; @@ -439,7 +444,7 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { minZoom = getInteger(sf2, zoomMatches, MINZOOM, 99); // Adjusted minZoom - if (sf.canBePolygon()) { + if (hasNamedPolygon) { // Emphasize large international airports earlier // Because the area grading resets the earlier dispensation if (kind.equals("aerodrome")) { @@ -478,7 +483,7 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { } // Assign outputFeature - if (sf.canBePolygon()) { + if (hasNamedPolygon) { outputFeature = features.pointOnSurface(this.name()) //.setAttr("area_debug", wayArea) // DEBUG .setAttr("elevation", sf.getString("ele")); From ed0399e6489c1d4445f40506a1d76ce542c21f1e Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Sun, 28 Dec 2025 10:00:34 -0800 Subject: [PATCH 28/33] Uppercased more constants --- .../com/protomaps/basemap/layers/Pois.java | 100 +++++++++--------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index 642523998..47a8a8e8c 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -252,12 +252,12 @@ public Pois(QrankDb qrankDb) { // Shorthand expressions to save space below - private static final Expression with_s_c = with(KIND, "cemetery", "school"); - private static final Expression with_n_p = with(KIND, "national_park"); - private static final Expression with_c_u = with(KIND, "college", "university"); - private static final Expression with_b_g = + private static final Expression WITH_S_C = with(KIND, "cemetery", "school"); + private static final Expression WITH_N_P = with(KIND, "national_park"); + private static final Expression WITH_C_U = with(KIND, "college", "university"); + private static final Expression WITH_B_G = with(KIND, "forest", "park", "protected_area", "nature_reserve", "village_green"); - private static final Expression with_etc = + private static final Expression WITH_ETC = with(KIND, "aerodrome", "golf_course", "military", "naval_base", "stadium", "zoo"); private static final MultiExpression.Index> namedPolygonZoomsIndex = @@ -302,63 +302,63 @@ public Pois(QrankDb qrankDb) { // Schools & Cemeteries - rule(with_s_c, withinRange(WAYAREA, 0, 10), use(MINZOOM, 16)), - rule(with_s_c, withinRange(WAYAREA, 10, 100), use(MINZOOM, 15)), - rule(with_s_c, withinRange(WAYAREA, 100, 1000), use(MINZOOM, 14)), - rule(with_s_c, withinRange(WAYAREA, 1000, 5000), use(MINZOOM, 13)), - rule(with_s_c, withinRange(WAYAREA, 5000), use(MINZOOM, 12)), + rule(WITH_S_C, withinRange(WAYAREA, 0, 10), use(MINZOOM, 16)), + rule(WITH_S_C, withinRange(WAYAREA, 10, 100), use(MINZOOM, 15)), + rule(WITH_S_C, withinRange(WAYAREA, 100, 1000), use(MINZOOM, 14)), + rule(WITH_S_C, withinRange(WAYAREA, 1000, 5000), use(MINZOOM, 13)), + rule(WITH_S_C, withinRange(WAYAREA, 5000), use(MINZOOM, 12)), // National parks - rule(with_n_p, withinRange(WAYAREA, 0, 250), use(MINZOOM, 17)), - rule(with_n_p, withinRange(WAYAREA, 250, 1000), use(MINZOOM, 14)), - rule(with_n_p, withinRange(WAYAREA, 1000, 5000), use(MINZOOM, 13)), - rule(with_n_p, withinRange(WAYAREA, 5000, 2e4), use(MINZOOM, 12)), - rule(with_n_p, withinRange(WAYAREA, 2e4, 1e5), use(MINZOOM, 11)), - rule(with_n_p, withinRange(WAYAREA, 1e5, 2.5e5), use(MINZOOM, 10)), - rule(with_n_p, withinRange(WAYAREA, 2.5e5, 2e6), use(MINZOOM, 9)), - rule(with_n_p, withinRange(WAYAREA, 2e6, 1e7), use(MINZOOM, 8)), - rule(with_n_p, withinRange(WAYAREA, 1e7, 2.5e7), use(MINZOOM, 7)), - rule(with_n_p, withinRange(WAYAREA, 2.5e7, 3e8), use(MINZOOM, 6)), - rule(with_n_p, withinRange(WAYAREA, 3e8), use(MINZOOM, 5)), + rule(WITH_N_P, withinRange(WAYAREA, 0, 250), use(MINZOOM, 17)), + rule(WITH_N_P, withinRange(WAYAREA, 250, 1000), use(MINZOOM, 14)), + rule(WITH_N_P, withinRange(WAYAREA, 1000, 5000), use(MINZOOM, 13)), + rule(WITH_N_P, withinRange(WAYAREA, 5000, 2e4), use(MINZOOM, 12)), + rule(WITH_N_P, withinRange(WAYAREA, 2e4, 1e5), use(MINZOOM, 11)), + rule(WITH_N_P, withinRange(WAYAREA, 1e5, 2.5e5), use(MINZOOM, 10)), + rule(WITH_N_P, withinRange(WAYAREA, 2.5e5, 2e6), use(MINZOOM, 9)), + rule(WITH_N_P, withinRange(WAYAREA, 2e6, 1e7), use(MINZOOM, 8)), + rule(WITH_N_P, withinRange(WAYAREA, 1e7, 2.5e7), use(MINZOOM, 7)), + rule(WITH_N_P, withinRange(WAYAREA, 2.5e7, 3e8), use(MINZOOM, 6)), + rule(WITH_N_P, withinRange(WAYAREA, 3e8), use(MINZOOM, 5)), // College and university polygons - rule(with_c_u, withinRange(WAYAREA, 0, 5000), use(MINZOOM, 15)), - rule(with_c_u, withinRange(WAYAREA, 5000, 2e4), use(MINZOOM, 14)), - rule(with_c_u, withinRange(WAYAREA, 2e4, 5e4), use(MINZOOM, 13)), - rule(with_c_u, withinRange(WAYAREA, 5e4, 1e5), use(MINZOOM, 12)), - rule(with_c_u, withinRange(WAYAREA, 1e5, 1.5e5), use(MINZOOM, 11)), - rule(with_c_u, withinRange(WAYAREA, 1.5e5, 2.5e5), use(MINZOOM, 10)), - rule(with_c_u, withinRange(WAYAREA, 2.5e5, 5e6), use(MINZOOM, 9)), - rule(with_c_u, withinRange(WAYAREA, 5e6, 2e7), use(MINZOOM, 8)), - rule(with_c_u, withinRange(WAYAREA, 2e7), use(MINZOOM, 7)), - rule(with_c_u, with("name", "Academy of Art University"), use(MINZOOM, 14)), // Hack for weird San Francisco university + rule(WITH_C_U, withinRange(WAYAREA, 0, 5000), use(MINZOOM, 15)), + rule(WITH_C_U, withinRange(WAYAREA, 5000, 2e4), use(MINZOOM, 14)), + rule(WITH_C_U, withinRange(WAYAREA, 2e4, 5e4), use(MINZOOM, 13)), + rule(WITH_C_U, withinRange(WAYAREA, 5e4, 1e5), use(MINZOOM, 12)), + rule(WITH_C_U, withinRange(WAYAREA, 1e5, 1.5e5), use(MINZOOM, 11)), + rule(WITH_C_U, withinRange(WAYAREA, 1.5e5, 2.5e5), use(MINZOOM, 10)), + rule(WITH_C_U, withinRange(WAYAREA, 2.5e5, 5e6), use(MINZOOM, 9)), + rule(WITH_C_U, withinRange(WAYAREA, 5e6, 2e7), use(MINZOOM, 8)), + rule(WITH_C_U, withinRange(WAYAREA, 2e7), use(MINZOOM, 7)), + rule(WITH_C_U, with("name", "Academy of Art University"), use(MINZOOM, 14)), // Hack for weird San Francisco university // Big green polygons - rule(with_b_g, withinRange(WAYAREA, 0, 1), use(MINZOOM, 17)), - rule(with_b_g, withinRange(WAYAREA, 1, 10), use(MINZOOM, 16)), - rule(with_b_g, withinRange(WAYAREA, 10, 250), use(MINZOOM, 15)), - rule(with_b_g, withinRange(WAYAREA, 250, 1000), use(MINZOOM, 14)), - rule(with_b_g, withinRange(WAYAREA, 1000, 5000), use(MINZOOM, 13)), - rule(with_b_g, withinRange(WAYAREA, 5000, 1.5e4), use(MINZOOM, 12)), - rule(with_b_g, withinRange(WAYAREA, 1.5e4, 2.5e5), use(MINZOOM, 11)), - rule(with_b_g, withinRange(WAYAREA, 2.5e5, 1e6), use(MINZOOM, 10)), - rule(with_b_g, withinRange(WAYAREA, 1e6, 4e6), use(MINZOOM, 9)), - rule(with_b_g, withinRange(WAYAREA, 4e6, 1e7), use(MINZOOM, 8)), - rule(with_b_g, withinRange(WAYAREA, 1e7), use(MINZOOM, 7)), + rule(WITH_B_G, withinRange(WAYAREA, 0, 1), use(MINZOOM, 17)), + rule(WITH_B_G, withinRange(WAYAREA, 1, 10), use(MINZOOM, 16)), + rule(WITH_B_G, withinRange(WAYAREA, 10, 250), use(MINZOOM, 15)), + rule(WITH_B_G, withinRange(WAYAREA, 250, 1000), use(MINZOOM, 14)), + rule(WITH_B_G, withinRange(WAYAREA, 1000, 5000), use(MINZOOM, 13)), + rule(WITH_B_G, withinRange(WAYAREA, 5000, 1.5e4), use(MINZOOM, 12)), + rule(WITH_B_G, withinRange(WAYAREA, 1.5e4, 2.5e5), use(MINZOOM, 11)), + rule(WITH_B_G, withinRange(WAYAREA, 2.5e5, 1e6), use(MINZOOM, 10)), + rule(WITH_B_G, withinRange(WAYAREA, 1e6, 4e6), use(MINZOOM, 9)), + rule(WITH_B_G, withinRange(WAYAREA, 4e6, 1e7), use(MINZOOM, 8)), + rule(WITH_B_G, withinRange(WAYAREA, 1e7), use(MINZOOM, 7)), // Remaining grab-bag of scaled kinds - rule(with_etc, withinRange(WAYAREA, 250, 1000), use(MINZOOM, 14)), - rule(with_etc, withinRange(WAYAREA, 1000, 5000), use(MINZOOM, 13)), - rule(with_etc, withinRange(WAYAREA, 5000, 2e4), use(MINZOOM, 12)), - rule(with_etc, withinRange(WAYAREA, 2e4, 1e5), use(MINZOOM, 11)), - rule(with_etc, withinRange(WAYAREA, 1e5, 2.5e5), use(MINZOOM, 10)), - rule(with_etc, withinRange(WAYAREA, 2.5e5, 5e6), use(MINZOOM, 9)), - rule(with_etc, withinRange(WAYAREA, 5e6, 2e7), use(MINZOOM, 8)), - rule(with_etc, withinRange(WAYAREA, 2e7), use(MINZOOM, 7)) + rule(WITH_ETC, withinRange(WAYAREA, 250, 1000), use(MINZOOM, 14)), + rule(WITH_ETC, withinRange(WAYAREA, 1000, 5000), use(MINZOOM, 13)), + rule(WITH_ETC, withinRange(WAYAREA, 5000, 2e4), use(MINZOOM, 12)), + rule(WITH_ETC, withinRange(WAYAREA, 2e4, 1e5), use(MINZOOM, 11)), + rule(WITH_ETC, withinRange(WAYAREA, 1e5, 2.5e5), use(MINZOOM, 10)), + rule(WITH_ETC, withinRange(WAYAREA, 2.5e5, 5e6), use(MINZOOM, 9)), + rule(WITH_ETC, withinRange(WAYAREA, 5e6, 2e7), use(MINZOOM, 8)), + rule(WITH_ETC, withinRange(WAYAREA, 2e7), use(MINZOOM, 7)) )).index(); From da5e34fce77dcb76b5225c856a8a761be342785b Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Mon, 29 Dec 2025 09:48:03 -0800 Subject: [PATCH 29/33] Bumped version number --- CHANGELOG.md | 4 ++++ tiles/src/main/java/com/protomaps/basemap/Basemap.java | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c7be0909..94b42d9a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +Tiles 4.13.6 +------ +- Translate POI min_zoom= assignments to MultiExpression rules [#539] + Tiles 4.13.5 ------ - Translate POI kind= assignments to MultiExpression rules [#537] diff --git a/tiles/src/main/java/com/protomaps/basemap/Basemap.java b/tiles/src/main/java/com/protomaps/basemap/Basemap.java index 6a4bc3406..74abfc687 100644 --- a/tiles/src/main/java/com/protomaps/basemap/Basemap.java +++ b/tiles/src/main/java/com/protomaps/basemap/Basemap.java @@ -119,7 +119,7 @@ public String description() { @Override public String version() { - return "4.13.5"; + return "4.13.6"; } @Override From a349c4b6f951eb86c1eb77073bb7580f38a9a038 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Mon, 29 Dec 2025 10:19:10 -0800 Subject: [PATCH 30/33] Applied automated code quality suggestions --- .../protomaps/basemap/feature/Matcher.java | 11 +++++------ .../com/protomaps/basemap/layers/Pois.java | 19 ++++++++----------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java b/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java index 36646ef5b..5fb6b4177 100644 --- a/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java +++ b/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java @@ -172,36 +172,35 @@ public static Expression without(String... arguments) { * @return An {@link Expression} for the numeric range check. */ public static Expression withinRange(String tagName, Integer lowerBound, Integer upperBound) { - return new WithinRangeExpression(tagName, new Long(lowerBound), new Long(upperBound)); + return new WithinRangeExpression(tagName, Long.valueOf(lowerBound), Long.valueOf(upperBound)); } /** * Overload withinRange to accept just lower bound integer */ public static Expression withinRange(String tagName, Integer lowerBound) { - return new WithinRangeExpression(tagName, new Long(lowerBound), null); + return new WithinRangeExpression(tagName, Long.valueOf(lowerBound), null); } /** * Overload withinRange to accept lower bound integer and upper bound double */ public static Expression withinRange(String tagName, Integer lowerBound, Double upperBound) { - return new WithinRangeExpression(tagName, new Long(lowerBound), Double.valueOf(upperBound).longValue()); + return new WithinRangeExpression(tagName, Long.valueOf(lowerBound), upperBound.longValue()); } /** * Overload withinRange to accept bounds as doubles */ public static Expression withinRange(String tagName, Double lowerBound, Double upperBound) { - return new WithinRangeExpression(tagName, Double.valueOf(lowerBound).longValue(), - Double.valueOf(upperBound).longValue()); + return new WithinRangeExpression(tagName, lowerBound.longValue(), upperBound.longValue()); } /** * Overload withinRange to accept just lower bound double */ public static Expression withinRange(String tagName, Double lowerBound) { - return new WithinRangeExpression(tagName, Double.valueOf(lowerBound).longValue(), null); + return new WithinRangeExpression(tagName, lowerBound.longValue(), null); } /** diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index 47a8a8e8c..2405924c2 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -378,9 +378,8 @@ private Boolean isNamedPolygon(SourceFeature sf) { public Matcher.SourceFeatureWithComputedTags computeExtraTags(SourceFeature sf, String kind) { Double wayArea = 0.0; Double height = 0.0; - Boolean hasNamedPolygon = isNamedPolygon(sf); - if (hasNamedPolygon) { + if (isNamedPolygon(sf)) { try { wayArea = sf.worldGeometry().getEnvelopeInternal().getArea() / WORLD_AREA_FOR_70_SQUARE_METERS; } catch (GeometryException e) { @@ -396,7 +395,7 @@ public Matcher.SourceFeatureWithComputedTags computeExtraTags(SourceFeature sf, Map computedTags; - if (hasNamedPolygon) { + if (isNamedPolygon(sf)) { computedTags = Map.of(KIND, kind, WAYAREA, wayArea, HEIGHT, height, HAS_NAMED_POLYGON, true); } else { computedTags = Map.of(KIND, kind, WAYAREA, wayArea, HEIGHT, height); @@ -406,10 +405,8 @@ public Matcher.SourceFeatureWithComputedTags computeExtraTags(SourceFeature sf, } public void processOsm(SourceFeature sf, FeatureCollector features) { - Boolean hasNamedPolygon = isNamedPolygon(sf); - // We only do POI display for points and named polygons - if (!sf.isPoint() && !hasNamedPolygon) + if (!sf.isPoint() && !isNamedPolygon(sf)) return; // Map the Protomaps KIND classification to incoming tags @@ -422,7 +419,7 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { Integer minZoom; // Quickly eliminate any features with non-matching tags - if (kind == UNDEFINED) + if (kind.equals(UNDEFINED)) return; // QRank may override minZoom entirely @@ -436,7 +433,7 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { } else { // Calculate minZoom using zooms indexes var sf2 = computeExtraTags(sf, getString(sf, kindMatches, KIND, UNDEFINED)); - var zoomMatches = hasNamedPolygon ? namedPolygonZoomsIndex.getMatches(sf2) : pointZoomsIndex.getMatches(sf2); + var zoomMatches = isNamedPolygon(sf) ? namedPolygonZoomsIndex.getMatches(sf2) : pointZoomsIndex.getMatches(sf2); if (zoomMatches.isEmpty()) return; @@ -444,7 +441,7 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { minZoom = getInteger(sf2, zoomMatches, MINZOOM, 99); // Adjusted minZoom - if (hasNamedPolygon) { + if (isNamedPolygon(sf)) { // Emphasize large international airports earlier // Because the area grading resets the earlier dispensation if (kind.equals("aerodrome")) { @@ -483,7 +480,7 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { } // Assign outputFeature - if (hasNamedPolygon) { + if (isNamedPolygon(sf)) { outputFeature = features.pointOnSurface(this.name()) //.setAttr("area_debug", wayArea) // DEBUG .setAttr("elevation", sf.getString("ele")); @@ -511,7 +508,7 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { .setAttr("iata", sf.getString("iata")); // Core Tilezen schema properties - if (kindDetail != UNDEFINED) + if (!kindDetail.equals(UNDEFINED)) outputFeature.setAttr("kind_detail", kindDetail); OsmNames.setOsmNames(outputFeature, sf, 0); From 5f9a59f1b3b858a3e5265018cb33e0c22f975a69 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Mon, 29 Dec 2025 10:36:44 -0800 Subject: [PATCH 31/33] Applied additional code quality suggestions --- .../com/protomaps/basemap/layers/Pois.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index 2405924c2..81d97c1fa 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -25,7 +25,6 @@ import java.util.List; import java.util.Map; - @SuppressWarnings("java:S1192") public class Pois implements ForwardingProfile.LayerPostProcessor { @@ -371,15 +370,16 @@ public String name() { private static final double WORLD_AREA_FOR_70_SQUARE_METERS = Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70)) / 256d, 2); - private Boolean isNamedPolygon(SourceFeature sf) { + private boolean isNamedPolygon(SourceFeature sf) { return sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null; } public Matcher.SourceFeatureWithComputedTags computeExtraTags(SourceFeature sf, String kind) { Double wayArea = 0.0; Double height = 0.0; + boolean hasNamedPolygon = isNamedPolygon(sf); - if (isNamedPolygon(sf)) { + if (hasNamedPolygon) { try { wayArea = sf.worldGeometry().getEnvelopeInternal().getArea() / WORLD_AREA_FOR_70_SQUARE_METERS; } catch (GeometryException e) { @@ -395,7 +395,7 @@ public Matcher.SourceFeatureWithComputedTags computeExtraTags(SourceFeature sf, Map computedTags; - if (isNamedPolygon(sf)) { + if (hasNamedPolygon) { computedTags = Map.of(KIND, kind, WAYAREA, wayArea, HEIGHT, height, HAS_NAMED_POLYGON, true); } else { computedTags = Map.of(KIND, kind, WAYAREA, wayArea, HEIGHT, height); @@ -405,8 +405,10 @@ public Matcher.SourceFeatureWithComputedTags computeExtraTags(SourceFeature sf, } public void processOsm(SourceFeature sf, FeatureCollector features) { + boolean hasNamedPolygon = isNamedPolygon(sf); + // We only do POI display for points and named polygons - if (!sf.isPoint() && !isNamedPolygon(sf)) + if (!sf.isPoint() && !hasNamedPolygon) return; // Map the Protomaps KIND classification to incoming tags @@ -433,7 +435,7 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { } else { // Calculate minZoom using zooms indexes var sf2 = computeExtraTags(sf, getString(sf, kindMatches, KIND, UNDEFINED)); - var zoomMatches = isNamedPolygon(sf) ? namedPolygonZoomsIndex.getMatches(sf2) : pointZoomsIndex.getMatches(sf2); + var zoomMatches = hasNamedPolygon ? namedPolygonZoomsIndex.getMatches(sf2) : pointZoomsIndex.getMatches(sf2); if (zoomMatches.isEmpty()) return; @@ -441,7 +443,7 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { minZoom = getInteger(sf2, zoomMatches, MINZOOM, 99); // Adjusted minZoom - if (isNamedPolygon(sf)) { + if (hasNamedPolygon) { // Emphasize large international airports earlier // Because the area grading resets the earlier dispensation if (kind.equals("aerodrome")) { @@ -480,7 +482,7 @@ public void processOsm(SourceFeature sf, FeatureCollector features) { } // Assign outputFeature - if (isNamedPolygon(sf)) { + if (hasNamedPolygon) { outputFeature = features.pointOnSurface(this.name()) //.setAttr("area_debug", wayArea) // DEBUG .setAttr("elevation", sf.getString("ele")); From 85d697773be2ca02c0359f898b2c8468c59998d5 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Mon, 29 Dec 2025 10:37:20 -0800 Subject: [PATCH 32/33] Committed trailing space removal in comment blocks due to .editorconfig settings --- .../protomaps/basemap/feature/Matcher.java | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java b/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java index 5fb6b4177..3de89f504 100644 --- a/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java +++ b/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java @@ -14,23 +14,23 @@ /** * A utility class for matching source feature properties to values. - * + * *

* Use the {@link #rule} function to create entries for a Planetiler {@link MultiExpression}. A rule consists of * multiple contitions that get joined by a logical AND, and key-value pairs that should be used if all conditions of * the rule are true. The key-value pairs of rules that get added later override the key-value pairs of rules that were * added earlier. *

- * + * *

* The MultiExpression can be used on a source feature and the resulting list of matches can be used in * {@link #getString} and similar functions to retrieve a value. *

- * + * *

* Example usage: *

- * + * *
  * 
  *var index = MultiExpression.ofOrdered(List.of(rule(with("highway", "primary"), use("kind", "major_road")))).index();
@@ -44,16 +44,16 @@ public record Use(String key, Object value) {}
 
   /**
    * Creates a matching rule with conditions and values.
-   * 
+   *
    * 

* Create conditions by calling the {@link #with} or {@link #without} functions. All conditions are joined by a * logical AND. *

- * + * *

* Create key-value pairs with the {@link #use} function. *

- * + * * @param arguments A mix of {@link Use} instances for key-value pairs and {@link Expression} instances for * conditions. * @return A {@link MultiExpression.Entry} containing the rule definition. @@ -73,13 +73,13 @@ public static MultiExpression.Entry> rule(Object... argument /** * Creates a {@link Use} instance representing a key-value pair to be supplied to the {@link #rule} function. - * + * *

* While in principle any Object can be supplied as value, retrievalbe later on are only Strings with * {@link #getString}, Integers with {@link #getInteger}, Doubles with {@link #getDouble}, Booleans with * {@link #getBoolean}. *

- * + * * @param key The key. * @param value The value associated with the key. * @return A new {@link Use} instance. @@ -90,30 +90,30 @@ public static Use use(String key, Object value) { /** * Creates an {@link Expression} that matches any of the specified arguments. - * + * *

* If no argument is supplied, matches everything. *

- * + * *

* If one argument is supplied, matches all source features that have this tag, e.g., {@code with("highway")} matches * to all source features with a highway tag. *

- * + * *

* If two arguments are supplied, matches to all source features that have this tag-value pair, e.g., * {@code with("highway", "primary")} matches to all source features with highway=primary. *

- * + * *

* If more than two arguments are supplied, matches to all source features that have the first argument as tag and the * later arguments as possible values, e.g., {@code with("highway", "primary", "secondary")} matches to all source * features that have highway=primary or highway=secondary. *

- * + * *

* If an argument consists of multiple lines, it will be broken up into one argument per line. Example: - * + * *

    * 
    * with("""
@@ -124,7 +124,7 @@ public static Use use(String key, Object value) {
    * 
    * 
*

- * + * * @param arguments Field names to match. * @return An {@link Expression} for the given field names. */ @@ -251,15 +251,15 @@ public record FromTag(String key) {} /** * Creates a {@link FromTag} instance representing a tag reference. - * + * *

* Use this function if to retrieve a value from a source feature when calling {@link #getString} and similar. *

- * + * *

* Example usage: *

- * + * *
    * 
    *var index = MultiExpression.ofOrdered(List.of(rule(with("highway", "primary", "secondary"), use("kind", fromTag("highway"))))).index();
@@ -269,7 +269,7 @@ public record FromTag(String key) {}
    * 
*

* On a source feature with highway=primary the above will result in kind=primary. - * + * * @param key The key of the tag. * @return A new {@link FromTag} instance. */ From 792ec922742f465453c25964d6b8e54767506caf Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Mon, 5 Jan 2026 11:06:31 -0800 Subject: [PATCH 33/33] Replaced single-bound version of withinRange() by atLeast() --- .../protomaps/basemap/feature/Matcher.java | 32 +++++++++++-------- .../com/protomaps/basemap/layers/Pois.java | 17 +++++----- .../basemap/feature/MatcherTest.java | 7 ++-- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java b/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java index 3de89f504..68f4d4565 100644 --- a/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java +++ b/tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java @@ -159,29 +159,18 @@ public static Expression without(String... arguments) { *

* *

- * If the upper bound is null, only the lower bound is checked (value >= lowerBound). - *

- * - *

* Tag values that cannot be parsed as numbers or missing tags will not match. *

* * @param tagName The name of the tag to check. * @param lowerBound The inclusive lower bound. - * @param upperBound The exclusive upper bound, or null to check only the lower bound. + * @param upperBound The exclusive upper bound. * @return An {@link Expression} for the numeric range check. */ public static Expression withinRange(String tagName, Integer lowerBound, Integer upperBound) { return new WithinRangeExpression(tagName, Long.valueOf(lowerBound), Long.valueOf(upperBound)); } - /** - * Overload withinRange to accept just lower bound integer - */ - public static Expression withinRange(String tagName, Integer lowerBound) { - return new WithinRangeExpression(tagName, Long.valueOf(lowerBound), null); - } - /** * Overload withinRange to accept lower bound integer and upper bound double */ @@ -197,9 +186,24 @@ public static Expression withinRange(String tagName, Double lowerBound, Double u } /** - * Overload withinRange to accept just lower bound double + * Creates an {@link Expression} that matches when a numeric tag value is greater or equal to a value. + * + *

+ * Tag values that cannot be parsed as numbers or missing tags will not match. + *

+ * + * @param tagName The name of the tag to check. + * @param lowerBound The inclusive lower bound. + * @return An {@link Expression} for the numeric range check. + */ + public static Expression atLeast(String tagName, Integer lowerBound) { + return new WithinRangeExpression(tagName, Long.valueOf(lowerBound), null); + } + + /** + * Overload atLeast to accept just lower bound double */ - public static Expression withinRange(String tagName, Double lowerBound) { + public static Expression atLeast(String tagName, Double lowerBound) { return new WithinRangeExpression(tagName, lowerBound.longValue(), null); } diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index 81d97c1fa..1a07dc899 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -1,6 +1,7 @@ package com.protomaps.basemap.layers; import static com.onthegomap.planetiler.util.Parse.parseDoubleOrNull; +import static com.protomaps.basemap.feature.Matcher.atLeast; import static com.protomaps.basemap.feature.Matcher.fromTag; import static com.protomaps.basemap.feature.Matcher.getInteger; import static com.protomaps.basemap.feature.Matcher.getString; @@ -270,11 +271,11 @@ public Pois(QrankDb qrankDb) { rule(withinRange(WAYAREA, 10, 500), use(MINZOOM, 14)), rule(withinRange(WAYAREA, 500, 2000), use(MINZOOM, 13)), rule(withinRange(WAYAREA, 2000, 1e4), use(MINZOOM, 12)), - rule(withinRange(WAYAREA, 1e4), use(MINZOOM, 11)), + rule(atLeast(WAYAREA, 1e4), use(MINZOOM, 11)), rule(with(KIND, "playground"), use(MINZOOM, 17)), rule(with(KIND, "allotments"), withinRange(WAYAREA, 0, 10), use(MINZOOM, 16)), - rule(with(KIND, "allotments"), withinRange(WAYAREA, 10), use(MINZOOM, 15)), + rule(with(KIND, "allotments"), atLeast(WAYAREA, 10), use(MINZOOM, 15)), // Height-graded polygons, generic at first then per-kind adjustments // Small but tall features should show up early as they have regional prominence. @@ -282,7 +283,7 @@ public Pois(QrankDb qrankDb) { rule(withinRange(WAYAREA, 10, 2000), withinRange(HEIGHT, 10, 20), use(MINZOOM, 13)), rule(withinRange(WAYAREA, 10, 2000), withinRange(HEIGHT, 20, 100), use(MINZOOM, 12)), - rule(withinRange(WAYAREA, 10, 2000), withinRange(HEIGHT, 100), use(MINZOOM, 11)), + rule(withinRange(WAYAREA, 10, 2000), atLeast(HEIGHT, 100), use(MINZOOM, 11)), // Clamp certain kind values so medium tall buildings don't crowd downtown areas // NOTE: (nvkelso 20230623) Apply label grid to early zooms of POIs layer @@ -305,7 +306,7 @@ public Pois(QrankDb qrankDb) { rule(WITH_S_C, withinRange(WAYAREA, 10, 100), use(MINZOOM, 15)), rule(WITH_S_C, withinRange(WAYAREA, 100, 1000), use(MINZOOM, 14)), rule(WITH_S_C, withinRange(WAYAREA, 1000, 5000), use(MINZOOM, 13)), - rule(WITH_S_C, withinRange(WAYAREA, 5000), use(MINZOOM, 12)), + rule(WITH_S_C, atLeast(WAYAREA, 5000), use(MINZOOM, 12)), // National parks @@ -319,7 +320,7 @@ public Pois(QrankDb qrankDb) { rule(WITH_N_P, withinRange(WAYAREA, 2e6, 1e7), use(MINZOOM, 8)), rule(WITH_N_P, withinRange(WAYAREA, 1e7, 2.5e7), use(MINZOOM, 7)), rule(WITH_N_P, withinRange(WAYAREA, 2.5e7, 3e8), use(MINZOOM, 6)), - rule(WITH_N_P, withinRange(WAYAREA, 3e8), use(MINZOOM, 5)), + rule(WITH_N_P, atLeast(WAYAREA, 3e8), use(MINZOOM, 5)), // College and university polygons @@ -331,7 +332,7 @@ public Pois(QrankDb qrankDb) { rule(WITH_C_U, withinRange(WAYAREA, 1.5e5, 2.5e5), use(MINZOOM, 10)), rule(WITH_C_U, withinRange(WAYAREA, 2.5e5, 5e6), use(MINZOOM, 9)), rule(WITH_C_U, withinRange(WAYAREA, 5e6, 2e7), use(MINZOOM, 8)), - rule(WITH_C_U, withinRange(WAYAREA, 2e7), use(MINZOOM, 7)), + rule(WITH_C_U, atLeast(WAYAREA, 2e7), use(MINZOOM, 7)), rule(WITH_C_U, with("name", "Academy of Art University"), use(MINZOOM, 14)), // Hack for weird San Francisco university // Big green polygons @@ -346,7 +347,7 @@ public Pois(QrankDb qrankDb) { rule(WITH_B_G, withinRange(WAYAREA, 2.5e5, 1e6), use(MINZOOM, 10)), rule(WITH_B_G, withinRange(WAYAREA, 1e6, 4e6), use(MINZOOM, 9)), rule(WITH_B_G, withinRange(WAYAREA, 4e6, 1e7), use(MINZOOM, 8)), - rule(WITH_B_G, withinRange(WAYAREA, 1e7), use(MINZOOM, 7)), + rule(WITH_B_G, atLeast(WAYAREA, 1e7), use(MINZOOM, 7)), // Remaining grab-bag of scaled kinds @@ -357,7 +358,7 @@ public Pois(QrankDb qrankDb) { rule(WITH_ETC, withinRange(WAYAREA, 1e5, 2.5e5), use(MINZOOM, 10)), rule(WITH_ETC, withinRange(WAYAREA, 2.5e5, 5e6), use(MINZOOM, 9)), rule(WITH_ETC, withinRange(WAYAREA, 5e6, 2e7), use(MINZOOM, 8)), - rule(WITH_ETC, withinRange(WAYAREA, 2e7), use(MINZOOM, 7)) + rule(WITH_ETC, atLeast(WAYAREA, 2e7), use(MINZOOM, 7)) )).index(); diff --git a/tiles/src/test/java/com/protomaps/basemap/feature/MatcherTest.java b/tiles/src/test/java/com/protomaps/basemap/feature/MatcherTest.java index 4fd641bac..81e17ffed 100644 --- a/tiles/src/test/java/com/protomaps/basemap/feature/MatcherTest.java +++ b/tiles/src/test/java/com/protomaps/basemap/feature/MatcherTest.java @@ -3,6 +3,7 @@ import static com.onthegomap.planetiler.TestUtils.newLineString; import static com.onthegomap.planetiler.TestUtils.newPoint; import static com.onthegomap.planetiler.TestUtils.newPolygon; +import static com.protomaps.basemap.feature.Matcher.atLeast; import static com.protomaps.basemap.feature.Matcher.fromTag; import static com.protomaps.basemap.feature.Matcher.getBoolean; import static com.protomaps.basemap.feature.Matcher.getDouble; @@ -704,7 +705,7 @@ void testGetBooleanFromTag() { } @Test - void testWithinRangeWithUpperBound() { + void testWithinRange() { var expression = withinRange("population", 5, 10); // Value within range (5 < 7 <= 10) @@ -759,8 +760,8 @@ void testWithinRangeWithUpperBound() { } @Test - void testWithinRangeWithoutUpperBound() { - var expression = withinRange("population", 5); + void testAtLeast() { + var expression = atLeast("population", 5); // Value above lower bound var sf = SimpleFeature.create(