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
*/
- 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(