Skip to content

Commit bdcff5d

Browse files
authored
matcher (#399)
* Add Matcher class * Use Matcher on landuse layer * use leisure in landuse * Add matcher test
1 parent 90abe15 commit bdcff5d

File tree

3 files changed

+1013
-133
lines changed

3 files changed

+1013
-133
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
package com.protomaps.basemap.feature;
2+
3+
import com.onthegomap.planetiler.expression.Expression;
4+
import com.onthegomap.planetiler.expression.MultiExpression;
5+
import com.onthegomap.planetiler.reader.SourceFeature;
6+
import java.util.ArrayList;
7+
import java.util.Arrays;
8+
import java.util.HashMap;
9+
import java.util.List;
10+
import java.util.Map;
11+
12+
/**
13+
* A utility class for matching source feature properties to values.
14+
*
15+
* <p>
16+
* Use the {@link #rule} function to create entries for a Planetiler {@link MultiExpression}. A rule consists of
17+
* multiple contitions that get joined by a logical AND, and key-value pairs that should be used if all conditions of
18+
* the rule are true. The key-value pairs of rules that get added later override the key-value pairs of rules that were
19+
* added earlier.
20+
* </p>
21+
*
22+
* <p>
23+
* The MultiExpression can be used on a source feature and the resulting list of matches can be used in
24+
* {@link #getString} and similar functions to retrieve a value.
25+
* </p>
26+
*
27+
* <p>
28+
* Example usage:
29+
* </p>
30+
*
31+
* <pre>
32+
* <code>
33+
*var index = MultiExpression.of(List.of(rule(with("highway", "primary"), use("kind", "major_road")))).index();
34+
*var matches = index.getMatches(sourceFeature);
35+
*String kind = getString(sourceFeature, matches, "kind", "other");
36+
* </code>
37+
* </pre>
38+
*/
39+
public class Matcher {
40+
public record Use(String key, Object value) {}
41+
42+
/**
43+
* Creates a matching rule with conditions and values.
44+
*
45+
* <p>
46+
* Create conditions by calling the {@link #with} or {@link #without} functions. All conditions are joined by a
47+
* logical AND.
48+
* </p>
49+
*
50+
* <p>
51+
* Create key-value pairs with the {@link #use} function.
52+
* </p>
53+
*
54+
* @param arguments A mix of {@link Use} instances for key-value pairs and {@link Expression} instances for
55+
* conditions.
56+
* @return A {@link MultiExpression.Entry} containing the rule definition.
57+
*/
58+
public static MultiExpression.Entry<Map<String, Object>> rule(Object... arguments) {
59+
Map<String, Object> result = new HashMap<>();
60+
List<Expression> conditions = new ArrayList<>();
61+
for (Object argument : arguments) {
62+
if (argument instanceof Use use) {
63+
result.put(use.key, use.value);
64+
} else if (argument instanceof Expression condition) {
65+
conditions.add(condition);
66+
}
67+
}
68+
return MultiExpression.entry(result, Expression.and(conditions));
69+
}
70+
71+
/**
72+
* Creates a {@link Use} instance representing a key-value pair to be supplied to the {@link #rule} function.
73+
*
74+
* <p>
75+
* While in principle any Object can be supplied as value, retrievalbe later on are only Strings with
76+
* {@link #getString}, Integers with {@link #getInteger}, Doubles with {@link #getDouble}, Booleans with
77+
* {@link #getBoolean}.
78+
* </p>
79+
*
80+
* @param key The key.
81+
* @param value The value associated with the key.
82+
* @return A new {@link Use} instance.
83+
*/
84+
public static Use use(String key, Object value) {
85+
return new Use(key, value);
86+
}
87+
88+
/**
89+
* Creates an {@link Expression} that matches any of the specified arguments.
90+
*
91+
* <p>
92+
* If no argument is supplied, matches everything.
93+
* </p>
94+
*
95+
* <p>
96+
* If one argument is supplied, matches all source features that have this tag, e.g., {@code with("highway")} matches
97+
* to all source features with a highway tag.
98+
* </p>
99+
*
100+
* <p>
101+
* If two arguments are supplied, matches to all source features that have this tag-value pair, e.g.,
102+
* {@code with("highway", "primary")} matches to all source features with highway=primary.
103+
* </p>
104+
*
105+
* <p>
106+
* If more than two arguments are supplied, matches to all source features that have the first argument as tag and the
107+
* later arguments as possible values, e.g., {@code with("highway", "primary", "secondary")} matches to all source
108+
* features that have highway=primary or highway=secondary.
109+
* </p>
110+
*
111+
* <p>
112+
* If an argument consists of multiple lines, it will be broken up into one argument per line. Example:
113+
*
114+
* <pre>
115+
* <code>
116+
* with("""
117+
* highway
118+
* primary
119+
* secondary
120+
* """)
121+
* </code>
122+
* </pre>
123+
* </p>
124+
*
125+
* @param arguments Field names to match.
126+
* @return An {@link Expression} for the given field names.
127+
*/
128+
public static Expression with(String... arguments) {
129+
130+
List<String> argumentList = Arrays.stream(arguments)
131+
.flatMap(String::lines)
132+
.map(String::trim)
133+
.filter(line -> !line.isBlank())
134+
.toList();
135+
136+
if (argumentList.isEmpty()) {
137+
return Expression.TRUE;
138+
} else if (argumentList.size() == 1) {
139+
return Expression.matchField(argumentList.getFirst());
140+
}
141+
return Expression.matchAny(argumentList.getFirst(), argumentList.subList(1, argumentList.size()));
142+
}
143+
144+
/**
145+
* Same as {@link #with}, but negated.
146+
*/
147+
public static Expression without(String... arguments) {
148+
return Expression.not(with(arguments));
149+
}
150+
151+
public record FromTag(String key) {}
152+
153+
/**
154+
* Creates a {@link FromTag} instance representing a tag reference.
155+
*
156+
* <p>
157+
* Use this function if to retrieve a value from a source feature when calling {@link #getString} and similar.
158+
* </p>
159+
*
160+
* <p>
161+
* Example usage:
162+
* </p>
163+
*
164+
* <pre>
165+
* <code>
166+
*var index = MultiExpression.of(List.of(rule(with("highway", "primary", "secondary"), use("kind", fromTag("highway"))))).index();
167+
*var matches = index.getMatches(sourceFeature);
168+
*String kind = getString(sourceFeature, matches, "kind", "other");
169+
* </code>
170+
* </pre>
171+
* <p>
172+
* On a source feature with highway=primary the above will result in kind=primary.
173+
*
174+
* @param key The key of the tag.
175+
* @return A new {@link FromTag} instance.
176+
*/
177+
public static FromTag fromTag(String key) {
178+
return new FromTag(key);
179+
}
180+
181+
public static String getString(SourceFeature sf, List<Map<String, Object>> matches, String key, String defaultValue) {
182+
for (var match : matches.reversed()) {
183+
if (match.containsKey(key)) {
184+
Object value = match.get(key);
185+
if (value instanceof String stringValue) {
186+
return stringValue;
187+
} else if (value instanceof FromTag fromTag) {
188+
return sf.getString(fromTag.key, defaultValue);
189+
} else {
190+
return defaultValue;
191+
}
192+
}
193+
}
194+
return defaultValue;
195+
}
196+
197+
public static Integer getInteger(SourceFeature sf, List<Map<String, Object>> matches, String key,
198+
Integer defaultValue) {
199+
for (var match : matches.reversed()) {
200+
if (match.containsKey(key)) {
201+
Object value = match.get(key);
202+
if (value instanceof Integer integerValue) {
203+
return integerValue;
204+
} else if (value instanceof FromTag fromTag) {
205+
try {
206+
return sf.hasTag(fromTag.key) ? Integer.valueOf(sf.getString(fromTag.key)) : defaultValue;
207+
} catch (NumberFormatException e) {
208+
return defaultValue;
209+
}
210+
} else {
211+
return defaultValue;
212+
}
213+
}
214+
}
215+
return defaultValue;
216+
}
217+
218+
public static Double getDouble(SourceFeature sf, List<Map<String, Object>> matches, String key, Double defaultValue) {
219+
for (var match : matches.reversed()) {
220+
if (match.containsKey(key)) {
221+
Object value = match.get(key);
222+
if (value instanceof Double doubleValue) {
223+
return doubleValue;
224+
} else if (value instanceof FromTag fromTag) {
225+
try {
226+
return sf.hasTag(fromTag.key) ? Double.valueOf(sf.getString(fromTag.key)) : defaultValue;
227+
} catch (NumberFormatException e) {
228+
return defaultValue;
229+
}
230+
} else {
231+
return defaultValue;
232+
}
233+
}
234+
}
235+
return defaultValue;
236+
}
237+
238+
public static Boolean getBoolean(SourceFeature sf, List<Map<String, Object>> matches, String key,
239+
Boolean defaultValue) {
240+
for (var match : matches.reversed()) {
241+
if (match.containsKey(key)) {
242+
Object value = match.get(key);
243+
if (value instanceof Boolean booleanValue) {
244+
return booleanValue;
245+
} else if (value instanceof FromTag fromTag) {
246+
return sf.hasTag(fromTag.key) ? sf.getBoolean(fromTag.key) : defaultValue;
247+
} else {
248+
return defaultValue;
249+
}
250+
}
251+
}
252+
return defaultValue;
253+
}
254+
255+
}

0 commit comments

Comments
 (0)