Skip to content

Commit 8b2df58

Browse files
authored
Migrate POI minZoom assigment from plain Java to MultiExpression rules (#539)
* Added base case POI zoom in new MultiExpression * Moved initial zoom assignments to MultiExpression, seeing one unexplained failure with aerodrome/iata tags * Moved a bunch of high-zoom point logic to MultiExpression * Created Matcher.SourceFeatureWithComputedTags() to allow mutation of tags for zoom checks * Fixed Matcher.SourceFeatureWithComputedTags() to require fewer signature changes * Moved SourceFeatureWithComputedTags construction into computeExtraTags * Moved selected small-area polygons into rules * Moved some protomaps-basemaps: tags to private static strings * Removed HashMap * Moved all college/university zooms to rules with new withinRange expression * Fixed zoom rule ordering failure * Moved assorted green and other zoom-based polygons to rules * Moved schools, cemeteries, and national parks to zoom-graded rules * Corrected withinRange() bounds logic and moved height adjustments to rules * Reorganized final adjustments for clarity * Shorthanded a bunch of zoom rules * Switched to overloaded withinRange() to allow for scientific notation in code * Moved last top-level tag checks in processOsm() to rules * De-duped some final logic to clarify identical point/polygon POI behavior * Renamed and organized zoom rules for legibility * Tweak, tweak, tweak * More tweak, tweak, tweak * Carved zoomsIndex into point-and-polygon-specific MultiExpressions * Fixed overly-broad point matches on irrelevant tags * Switched to constants for KIND and MINZOOM strings * Cleanup * Condensed some long booleans * Uppercased more constants * Bumped version number * Applied automated code quality suggestions * Applied additional code quality suggestions * Committed trailing space removal in comment blocks due to .editorconfig settings * Replaced single-bound version of withinRange() by atLeast()
1 parent 79ead62 commit 8b2df58

File tree

5 files changed

+751
-451
lines changed

5 files changed

+751
-451
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
Tiles 4.13.6
2+
------
3+
- Translate POI min_zoom= assignments to MultiExpression rules [#539]
4+
15
Tiles 4.13.5
26
------
37
- Translate POI kind= assignments to MultiExpression rules [#537]

tiles/src/main/java/com/protomaps/basemap/Basemap.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ public String description() {
119119

120120
@Override
121121
public String version() {
122-
return "4.13.5";
122+
return "4.13.6";
123123
}
124124

125125
@Override

tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java

Lines changed: 160 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,35 @@
22

33
import com.onthegomap.planetiler.expression.Expression;
44
import com.onthegomap.planetiler.expression.MultiExpression;
5+
import com.onthegomap.planetiler.geo.GeometryException;
56
import com.onthegomap.planetiler.geo.GeometryType;
67
import com.onthegomap.planetiler.reader.SourceFeature;
78
import java.util.ArrayList;
89
import java.util.Arrays;
910
import java.util.HashMap;
1011
import java.util.List;
1112
import java.util.Map;
13+
import org.locationtech.jts.geom.Geometry;
1214

1315
/**
1416
* A utility class for matching source feature properties to values.
15-
*
17+
*
1618
* <p>
1719
* Use the {@link #rule} function to create entries for a Planetiler {@link MultiExpression}. A rule consists of
1820
* multiple contitions that get joined by a logical AND, and key-value pairs that should be used if all conditions of
1921
* the rule are true. The key-value pairs of rules that get added later override the key-value pairs of rules that were
2022
* added earlier.
2123
* </p>
22-
*
24+
*
2325
* <p>
2426
* The MultiExpression can be used on a source feature and the resulting list of matches can be used in
2527
* {@link #getString} and similar functions to retrieve a value.
2628
* </p>
27-
*
29+
*
2830
* <p>
2931
* Example usage:
3032
* </p>
31-
*
33+
*
3234
* <pre>
3335
* <code>
3436
*var index = MultiExpression.ofOrdered(List.of(rule(with("highway", "primary"), use("kind", "major_road")))).index();
@@ -42,16 +44,16 @@ public record Use(String key, Object value) {}
4244

4345
/**
4446
* Creates a matching rule with conditions and values.
45-
*
47+
*
4648
* <p>
4749
* Create conditions by calling the {@link #with} or {@link #without} functions. All conditions are joined by a
4850
* logical AND.
4951
* </p>
50-
*
52+
*
5153
* <p>
5254
* Create key-value pairs with the {@link #use} function.
5355
* </p>
54-
*
56+
*
5557
* @param arguments A mix of {@link Use} instances for key-value pairs and {@link Expression} instances for
5658
* conditions.
5759
* @return A {@link MultiExpression.Entry} containing the rule definition.
@@ -71,13 +73,13 @@ public static MultiExpression.Entry<Map<String, Object>> rule(Object... argument
7173

7274
/**
7375
* Creates a {@link Use} instance representing a key-value pair to be supplied to the {@link #rule} function.
74-
*
76+
*
7577
* <p>
7678
* While in principle any Object can be supplied as value, retrievalbe later on are only Strings with
7779
* {@link #getString}, Integers with {@link #getInteger}, Doubles with {@link #getDouble}, Booleans with
7880
* {@link #getBoolean}.
7981
* </p>
80-
*
82+
*
8183
* @param key The key.
8284
* @param value The value associated with the key.
8385
* @return A new {@link Use} instance.
@@ -88,30 +90,30 @@ public static Use use(String key, Object value) {
8890

8991
/**
9092
* Creates an {@link Expression} that matches any of the specified arguments.
91-
*
93+
*
9294
* <p>
9395
* If no argument is supplied, matches everything.
9496
* </p>
95-
*
97+
*
9698
* <p>
9799
* If one argument is supplied, matches all source features that have this tag, e.g., {@code with("highway")} matches
98100
* to all source features with a highway tag.
99101
* </p>
100-
*
102+
*
101103
* <p>
102104
* If two arguments are supplied, matches to all source features that have this tag-value pair, e.g.,
103105
* {@code with("highway", "primary")} matches to all source features with highway=primary.
104106
* </p>
105-
*
107+
*
106108
* <p>
107109
* If more than two arguments are supplied, matches to all source features that have the first argument as tag and the
108110
* later arguments as possible values, e.g., {@code with("highway", "primary", "secondary")} matches to all source
109111
* features that have highway=primary or highway=secondary.
110112
* </p>
111-
*
113+
*
112114
* <p>
113115
* If an argument consists of multiple lines, it will be broken up into one argument per line. Example:
114-
*
116+
*
115117
* <pre>
116118
* <code>
117119
* with("""
@@ -122,7 +124,7 @@ public static Use use(String key, Object value) {
122124
* </code>
123125
* </pre>
124126
* </p>
125-
*
127+
*
126128
* @param arguments Field names to match.
127129
* @return An {@link Expression} for the given field names.
128130
*/
@@ -149,6 +151,82 @@ public static Expression without(String... arguments) {
149151
return Expression.not(with(arguments));
150152
}
151153

154+
/**
155+
* Creates an {@link Expression} that matches when a numeric tag value is within a specified range.
156+
*
157+
* <p>
158+
* The lower bound is inclusive. The upper bound, if provided, is exclusive.
159+
* </p>
160+
*
161+
* <p>
162+
* Tag values that cannot be parsed as numbers or missing tags will not match.
163+
* </p>
164+
*
165+
* @param tagName The name of the tag to check.
166+
* @param lowerBound The inclusive lower bound.
167+
* @param upperBound The exclusive upper bound.
168+
* @return An {@link Expression} for the numeric range check.
169+
*/
170+
public static Expression withinRange(String tagName, Integer lowerBound, Integer upperBound) {
171+
return new WithinRangeExpression(tagName, Long.valueOf(lowerBound), Long.valueOf(upperBound));
172+
}
173+
174+
/**
175+
* Overload withinRange to accept lower bound integer and upper bound double
176+
*/
177+
public static Expression withinRange(String tagName, Integer lowerBound, Double upperBound) {
178+
return new WithinRangeExpression(tagName, Long.valueOf(lowerBound), upperBound.longValue());
179+
}
180+
181+
/**
182+
* Overload withinRange to accept bounds as doubles
183+
*/
184+
public static Expression withinRange(String tagName, Double lowerBound, Double upperBound) {
185+
return new WithinRangeExpression(tagName, lowerBound.longValue(), upperBound.longValue());
186+
}
187+
188+
/**
189+
* Creates an {@link Expression} that matches when a numeric tag value is greater or equal to a value.
190+
*
191+
* <p>
192+
* Tag values that cannot be parsed as numbers or missing tags will not match.
193+
* </p>
194+
*
195+
* @param tagName The name of the tag to check.
196+
* @param lowerBound The inclusive lower bound.
197+
* @return An {@link Expression} for the numeric range check.
198+
*/
199+
public static Expression atLeast(String tagName, Integer lowerBound) {
200+
return new WithinRangeExpression(tagName, Long.valueOf(lowerBound), null);
201+
}
202+
203+
/**
204+
* Overload atLeast to accept just lower bound double
205+
*/
206+
public static Expression atLeast(String tagName, Double lowerBound) {
207+
return new WithinRangeExpression(tagName, lowerBound.longValue(), null);
208+
}
209+
210+
/**
211+
* Expression implementation for numeric range matching.
212+
*/
213+
private record WithinRangeExpression(String tagName, long lowerBound, Long upperBound) implements Expression {
214+
215+
@Override
216+
public boolean evaluate(com.onthegomap.planetiler.reader.WithTags input, List<String> matchKeys) {
217+
if (!input.hasTag(tagName)) {
218+
return false;
219+
}
220+
long value = input.getLong(tagName);
221+
// getLong returns 0 for invalid values, so we need to check if 0 is actually the tag value
222+
if (value == 0 && !"0".equals(input.getString(tagName))) {
223+
// getLong returned 0 because parsing failed
224+
return false;
225+
}
226+
return value >= lowerBound && (upperBound == null || value < upperBound);
227+
}
228+
}
229+
152230
public static Expression withPoint() {
153231
return Expression.matchGeometryType(GeometryType.POINT);
154232
}
@@ -177,15 +255,15 @@ public record FromTag(String key) {}
177255

178256
/**
179257
* Creates a {@link FromTag} instance representing a tag reference.
180-
*
258+
*
181259
* <p>
182260
* Use this function if to retrieve a value from a source feature when calling {@link #getString} and similar.
183261
* </p>
184-
*
262+
*
185263
* <p>
186264
* Example usage:
187265
* </p>
188-
*
266+
*
189267
* <pre>
190268
* <code>
191269
*var index = MultiExpression.ofOrdered(List.of(rule(with("highway", "primary", "secondary"), use("kind", fromTag("highway"))))).index();
@@ -195,7 +273,7 @@ public record FromTag(String key) {}
195273
* </pre>
196274
* <p>
197275
* On a source feature with highway=primary the above will result in kind=primary.
198-
*
276+
*
199277
* @param key The key of the tag.
200278
* @return A new {@link FromTag} instance.
201279
*/
@@ -277,4 +355,66 @@ public static Boolean getBoolean(SourceFeature sf, List<Map<String, Object>> mat
277355
return defaultValue;
278356
}
279357

358+
/**
359+
* Wrapper that combines a SourceFeature with computed tags without mutating the original. This allows MultiExpression
360+
* matching to access both original and computed tags.
361+
*
362+
* <p>
363+
* This is useful when you need to add computed tags (like area calculations or derived properties) that should be
364+
* accessible to MultiExpression rules, but the original SourceFeature has immutable tags.
365+
* </p>
366+
*/
367+
public static class SourceFeatureWithComputedTags extends SourceFeature {
368+
private final SourceFeature delegate;
369+
private final Map<String, Object> combinedTags;
370+
371+
/**
372+
* Creates a wrapper around a SourceFeature with additional computed tags.
373+
*
374+
* @param delegate The original SourceFeature to wrap
375+
* @param computedTags Additional computed tags to merge with the original tags
376+
*/
377+
public SourceFeatureWithComputedTags(SourceFeature delegate, Map<String, Object> computedTags) {
378+
super(new HashMap<>(delegate.tags()), delegate.getSource(), delegate.getSourceLayer(), null, delegate.id());
379+
this.delegate = delegate;
380+
this.combinedTags = new HashMap<>(delegate.tags());
381+
this.combinedTags.putAll(computedTags);
382+
}
383+
384+
@Override
385+
public Map<String, Object> tags() {
386+
return combinedTags;
387+
}
388+
389+
@Override
390+
public Geometry worldGeometry() throws GeometryException {
391+
return delegate.worldGeometry();
392+
}
393+
394+
@Override
395+
public Geometry latLonGeometry() throws GeometryException {
396+
return delegate.latLonGeometry();
397+
}
398+
399+
@Override
400+
public boolean isPoint() {
401+
return delegate.isPoint();
402+
}
403+
404+
@Override
405+
public boolean canBePolygon() {
406+
return delegate.canBePolygon();
407+
}
408+
409+
@Override
410+
public boolean canBeLine() {
411+
return delegate.canBeLine();
412+
}
413+
414+
@Override
415+
public boolean hasRelationInfo() {
416+
return delegate.hasRelationInfo();
417+
}
418+
}
419+
280420
}

0 commit comments

Comments
 (0)