Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7e70005
Added base case POI zoom in new MultiExpression
migurski Dec 26, 2025
3a62861
Moved initial zoom assignments to MultiExpression, seeing one unexpla…
migurski Dec 26, 2025
03316e2
Moved a bunch of high-zoom point logic to MultiExpression
migurski Dec 26, 2025
8821873
Created Matcher.SourceFeatureWithComputedTags() to allow mutation of …
migurski Dec 26, 2025
f7a4928
Fixed Matcher.SourceFeatureWithComputedTags() to require fewer signat…
migurski Dec 26, 2025
5c529a5
Moved SourceFeatureWithComputedTags construction into computeExtraTags
migurski Dec 26, 2025
5711a0e
Moved selected small-area polygons into rules
migurski Dec 26, 2025
8fa32b3
Moved some protomaps-basemaps: tags to private static strings
migurski Dec 26, 2025
8200a7c
Removed HashMap
migurski Dec 27, 2025
379cb91
Moved all college/university zooms to rules with new withinRange expr…
migurski Dec 27, 2025
d03004f
Fixed zoom rule ordering failure
migurski Dec 27, 2025
1ff3316
Moved assorted green and other zoom-based polygons to rules
migurski Dec 27, 2025
f2dbdf4
Moved schools, cemeteries, and national parks to zoom-graded rules
migurski Dec 27, 2025
cca5a64
Corrected withinRange() bounds logic and moved height adjustments to …
migurski Dec 27, 2025
4431fb3
Reorganized final adjustments for clarity
migurski Dec 27, 2025
1febd2e
Shorthanded a bunch of zoom rules
migurski Dec 27, 2025
1a59b25
Switched to overloaded withinRange() to allow for scientific notation…
migurski Dec 27, 2025
665645a
Moved last top-level tag checks in processOsm() to rules
migurski Dec 27, 2025
5dcb548
De-duped some final logic to clarify identical point/polygon POI beha…
migurski Dec 27, 2025
3a569b6
Renamed and organized zoom rules for legibility
migurski Dec 27, 2025
2525a5c
Tweak, tweak, tweak
migurski Dec 27, 2025
1982412
More tweak, tweak, tweak
migurski Dec 27, 2025
f0ea92a
Carved zoomsIndex into point-and-polygon-specific MultiExpressions
migurski Dec 27, 2025
479a5e4
Fixed overly-broad point matches on irrelevant tags
migurski Dec 27, 2025
415e587
Switched to constants for KIND and MINZOOM strings
migurski Dec 27, 2025
d619032
Cleanup
migurski Dec 27, 2025
2dfde30
Condensed some long booleans
migurski Dec 28, 2025
ed0399e
Uppercased more constants
migurski Dec 28, 2025
da5e34f
Bumped version number
migurski Dec 29, 2025
a349c4b
Applied automated code quality suggestions
migurski Dec 29, 2025
5f9a59f
Applied additional code quality suggestions
migurski Dec 29, 2025
85d6977
Committed trailing space removal in comment blocks due to .editorconf…
migurski Dec 29, 2025
792ec92
Replaced single-bound version of withinRange() by atLeast()
migurski Jan 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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]
Expand Down
2 changes: 1 addition & 1 deletion tiles/src/main/java/com/protomaps/basemap/Basemap.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public String description() {

@Override
public String version() {
return "4.13.5";
return "4.13.6";
}

@Override
Expand Down
180 changes: 160 additions & 20 deletions tiles/src/main/java/com/protomaps/basemap/feature/Matcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,35 @@

import com.onthegomap.planetiler.expression.Expression;
import com.onthegomap.planetiler.expression.MultiExpression;
import com.onthegomap.planetiler.geo.GeometryException;
import com.onthegomap.planetiler.geo.GeometryType;
import com.onthegomap.planetiler.reader.SourceFeature;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.locationtech.jts.geom.Geometry;

/**
* A utility class for matching source feature properties to values.
*
*
* <p>
* 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.
* </p>
*
*
* <p>
* 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.
* </p>
*
*
* <p>
* Example usage:
* </p>
*
*
* <pre>
* <code>
*var index = MultiExpression.ofOrdered(List.of(rule(with("highway", "primary"), use("kind", "major_road")))).index();
Expand All @@ -42,16 +44,16 @@ public record Use(String key, Object value) {}

/**
* Creates a matching rule with conditions and values.
*
*
* <p>
* Create conditions by calling the {@link #with} or {@link #without} functions. All conditions are joined by a
* logical AND.
* </p>
*
*
* <p>
* Create key-value pairs with the {@link #use} function.
* </p>
*
*
* @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.
Expand All @@ -71,13 +73,13 @@ public static MultiExpression.Entry<Map<String, Object>> rule(Object... argument

/**
* Creates a {@link Use} instance representing a key-value pair to be supplied to the {@link #rule} function.
*
*
* <p>
* 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}.
* </p>
*
*
* @param key The key.
* @param value The value associated with the key.
* @return A new {@link Use} instance.
Expand All @@ -88,30 +90,30 @@ public static Use use(String key, Object value) {

/**
* Creates an {@link Expression} that matches any of the specified arguments.
*
*
* <p>
* If no argument is supplied, matches everything.
* </p>
*
*
* <p>
* 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.
* </p>
*
*
* <p>
* 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.
* </p>
*
*
* <p>
* 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.
* </p>
*
*
* <p>
* If an argument consists of multiple lines, it will be broken up into one argument per line. Example:
*
*
* <pre>
* <code>
* with("""
Expand All @@ -122,7 +124,7 @@ public static Use use(String key, Object value) {
* </code>
* </pre>
* </p>
*
*
* @param arguments Field names to match.
* @return An {@link Expression} for the given field names.
*/
Expand All @@ -149,6 +151,82 @@ 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.
*
* <p>
* The lower bound is inclusive. The upper bound, if provided, is exclusive.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* The lower bound is inclusive. The upper bound, if provided, is exclusive.
* The lower bound is inclusive. The upper bound is exclusive.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this in #541

* </p>
*
* <p>
* Tag values that cannot be parsed as numbers or missing tags will not match.
* </p>
*
* @param tagName The name of the tag to check.
* @param lowerBound The inclusive 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 lower bound integer and upper bound double
*/
public static Expression withinRange(String tagName, Integer lowerBound, Double upperBound) {
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, lowerBound.longValue(), upperBound.longValue());
}

/**
* Creates an {@link Expression} that matches when a numeric tag value is greater or equal to a value.
*
* <p>
* Tag values that cannot be parsed as numbers or missing tags will not match.
* </p>
*
* @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 atLeast(String tagName, Double lowerBound) {
return new WithinRangeExpression(tagName, lowerBound.longValue(), null);
}

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

public static Expression withPoint() {
return Expression.matchGeometryType(GeometryType.POINT);
}
Expand Down Expand Up @@ -177,15 +255,15 @@ public record FromTag(String key) {}

/**
* Creates a {@link FromTag} instance representing a tag reference.
*
*
* <p>
* Use this function if to retrieve a value from a source feature when calling {@link #getString} and similar.
* </p>
*
*
* <p>
* Example usage:
* </p>
*
*
* <pre>
* <code>
*var index = MultiExpression.ofOrdered(List.of(rule(with("highway", "primary", "secondary"), use("kind", fromTag("highway"))))).index();
Expand All @@ -195,7 +273,7 @@ public record FromTag(String key) {}
* </pre>
* <p>
* 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.
*/
Expand Down Expand Up @@ -277,4 +355,66 @@ public static Boolean getBoolean(SourceFeature sf, List<Map<String, Object>> mat
return defaultValue;
}

/**
* Wrapper that combines a SourceFeature with computed tags without mutating the original. This allows MultiExpression
* matching to access both original and computed tags.
*
* <p>
* This is useful when you need to add computed tags (like area calculations or derived properties) that should be
* accessible to MultiExpression rules, but the original SourceFeature has immutable tags.
* </p>
*/
public static class SourceFeatureWithComputedTags extends SourceFeature {
private final SourceFeature delegate;
private final Map<String, Object> combinedTags;

/**
* Creates a wrapper around a SourceFeature with additional computed tags.
*
* @param delegate The original SourceFeature to wrap
* @param computedTags Additional computed tags to merge with the original tags
*/
public SourceFeatureWithComputedTags(SourceFeature delegate, Map<String, Object> 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);
}

@Override
public Map<String, Object> tags() {
return combinedTags;
}

@Override
public Geometry worldGeometry() throws GeometryException {
return delegate.worldGeometry();
}

@Override
public Geometry latLonGeometry() throws GeometryException {
return delegate.latLonGeometry();
}

@Override
public boolean isPoint() {
return delegate.isPoint();
}

@Override
public boolean canBePolygon() {
return delegate.canBePolygon();
}

@Override
public boolean canBeLine() {
return delegate.canBeLine();
}

@Override
public boolean hasRelationInfo() {
return delegate.hasRelationInfo();
}
}

}
Loading
Loading