22
33import com .onthegomap .planetiler .expression .Expression ;
44import com .onthegomap .planetiler .expression .MultiExpression ;
5+ import com .onthegomap .planetiler .geo .GeometryException ;
56import com .onthegomap .planetiler .geo .GeometryType ;
67import com .onthegomap .planetiler .reader .SourceFeature ;
78import java .util .ArrayList ;
89import java .util .Arrays ;
910import java .util .HashMap ;
1011import java .util .List ;
1112import 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