source: josm/trunk/src/org/openstreetmap/josm/gui/mappaint/mapcss/Condition.java@ 7167

Last change on this file since 7167 was 7167, checked in by simon04, 10 years ago

MapCSS: simplify ~= implementation, add unit tests

  • Property svn:eol-style set to native
File size: 14.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.mappaint.mapcss;
3
4import java.text.MessageFormat;
5import java.util.Arrays;
6import java.util.EnumSet;
7import java.util.Objects;
8import java.util.Set;
9import java.util.regex.Pattern;
10
11import org.openstreetmap.josm.data.osm.Node;
12import org.openstreetmap.josm.data.osm.OsmPrimitive;
13import org.openstreetmap.josm.data.osm.Relation;
14import org.openstreetmap.josm.data.osm.Tag;
15import org.openstreetmap.josm.data.osm.Way;
16import org.openstreetmap.josm.gui.mappaint.Cascade;
17import org.openstreetmap.josm.gui.mappaint.ElemStyles;
18import org.openstreetmap.josm.gui.mappaint.Environment;
19import org.openstreetmap.josm.tools.CheckParameterUtil;
20import org.openstreetmap.josm.tools.Predicate;
21import org.openstreetmap.josm.tools.Predicates;
22import org.openstreetmap.josm.tools.Utils;
23
24public abstract class Condition {
25
26 public abstract boolean applies(Environment e);
27
28 public static Condition createKeyValueCondition(String k, String v, Op op, Context context, boolean considerValAsKey) {
29 switch (context) {
30 case PRIMITIVE:
31 if (KeyValueRegexpCondition.SUPPORTED_OPS.contains(op) && !considerValAsKey)
32 return new KeyValueRegexpCondition(k, v, op, false);
33 if (!considerValAsKey && op.equals(Op.EQ))
34 return new SimpleKeyValueCondition(k, v);
35 return new KeyValueCondition(k, v, op, considerValAsKey);
36 case LINK:
37 if (considerValAsKey)
38 throw new MapCSSException("''considerValAsKey'' not supported in LINK context");
39 if ("role".equalsIgnoreCase(k))
40 return new RoleCondition(v, op);
41 else if ("index".equalsIgnoreCase(k))
42 return new IndexCondition(v, op);
43 else
44 throw new MapCSSException(
45 MessageFormat.format("Expected key ''role'' or ''index'' in link context. Got ''{0}''.", k));
46
47 default: throw new AssertionError();
48 }
49 }
50
51 public static Condition createKeyCondition(String k, boolean not, KeyMatchType matchType, Context context) {
52 switch (context) {
53 case PRIMITIVE:
54 return new KeyCondition(k, not, matchType);
55 case LINK:
56 if (matchType != null)
57 throw new MapCSSException("Question mark operator ''?'' and regexp match not supported in LINK context");
58 if (not)
59 return new RoleCondition(k, Op.NEQ);
60 else
61 return new RoleCondition(k, Op.EQ);
62
63 default: throw new AssertionError();
64 }
65 }
66
67 public static PseudoClassCondition createPseudoClassCondition(String id, boolean not, Context context) {
68 return new PseudoClassCondition(id, not, context);
69 }
70
71 public static ClassCondition createClassCondition(String id, boolean not, Context context) {
72 return new ClassCondition(id, not);
73 }
74
75 public static ExpressionCondition createExpressionCondition(Expression e, Context context) {
76 return new ExpressionCondition(e);
77 }
78
79 public static enum Op {
80 EQ, NEQ, GREATER_OR_EQUAL, GREATER, LESS_OR_EQUAL, LESS,
81 REGEX, NREGEX, ONE_OF, BEGINS_WITH, ENDS_WITH, CONTAINS;
82
83 private static final Set<Op> NEGATED_OPS = EnumSet.of(NEQ, NREGEX);
84
85 public boolean eval(String testString, String prototypeString) {
86 if (testString == null && !NEGATED_OPS.contains(this))
87 return false;
88 switch (this) {
89 case EQ:
90 return Objects.equals(testString, prototypeString);
91 case NEQ:
92 return !Objects.equals(testString, prototypeString);
93 case REGEX:
94 case NREGEX:
95 final boolean contains = Pattern.compile(prototypeString).matcher(testString).find();
96 return REGEX.equals(this) ? contains : !contains;
97 case ONE_OF:
98 return Arrays.asList(testString.split("\\s*;\\s*")).contains(prototypeString);
99 case BEGINS_WITH:
100 return testString.startsWith(prototypeString);
101 case ENDS_WITH:
102 return testString.endsWith(prototypeString);
103 case CONTAINS:
104 return testString.contains(prototypeString);
105 }
106
107 float test_float;
108 try {
109 test_float = Float.parseFloat(testString);
110 } catch (NumberFormatException e) {
111 return false;
112 }
113 float prototype_float = Float.parseFloat(prototypeString);
114
115 switch (this) {
116 case GREATER_OR_EQUAL:
117 return test_float >= prototype_float;
118 case GREATER:
119 return test_float > prototype_float;
120 case LESS_OR_EQUAL:
121 return test_float <= prototype_float;
122 case LESS:
123 return test_float < prototype_float;
124 default:
125 throw new AssertionError();
126 }
127 }
128 }
129
130 /**
131 * Context, where the condition applies.
132 */
133 public static enum Context {
134 /**
135 * normal primitive selector, e.g. way[highway=residential]
136 */
137 PRIMITIVE,
138
139 /**
140 * link between primitives, e.g. relation &gt;[role=outer] way
141 */
142 LINK
143 }
144
145 public static final EnumSet<Op> COMPARISON_OPERATERS =
146 EnumSet.of(Op.GREATER_OR_EQUAL, Op.GREATER, Op.LESS_OR_EQUAL, Op.LESS);
147
148 /**
149 * Most common case of a KeyValueCondition.
150 *
151 * Extra class for performance reasons.
152 */
153 public static class SimpleKeyValueCondition extends Condition {
154 public final String k;
155 public final String v;
156
157 public SimpleKeyValueCondition(String k, String v) {
158 this.k = k;
159 this.v = v;
160 }
161
162 @Override
163 public boolean applies(Environment e) {
164 return v.equals(e.osm.get(k));
165 }
166
167 public Tag asTag() {
168 return new Tag(k, v);
169 }
170
171 @Override
172 public String toString() {
173 return '[' + k + '=' + v + ']';
174 }
175
176 }
177
178 /**
179 * <p>Represents a key/value condition which is either applied to a primitive.</p>
180 *
181 */
182 public static class KeyValueCondition extends Condition {
183
184 public final String k;
185 public final String v;
186 public final Op op;
187 public boolean considerValAsKey;
188
189 /**
190 * <p>Creates a key/value-condition.</p>
191 *
192 * @param k the key
193 * @param v the value
194 * @param op the operation
195 * @param considerValAsKey whether to consider {@code v} as another key and compare the values of key {@code k} and key {@code v}.
196 */
197 public KeyValueCondition(String k, String v, Op op, boolean considerValAsKey) {
198 this.k = k;
199 this.v = v;
200 this.op = op;
201 this.considerValAsKey = considerValAsKey;
202 }
203
204 @Override
205 public boolean applies(Environment env) {
206 return op.eval(env.osm.get(k), considerValAsKey ? env.osm.get(v) : v);
207 }
208
209 public Tag asTag() {
210 return new Tag(k, v);
211 }
212
213 @Override
214 public String toString() {
215 return "[" + k + "'" + op + "'" + v + "]";
216 }
217 }
218
219 public static class KeyValueRegexpCondition extends KeyValueCondition {
220
221 public final Pattern pattern;
222 public static final EnumSet<Op> SUPPORTED_OPS = EnumSet.of(Op.REGEX, Op.NREGEX);
223
224 public KeyValueRegexpCondition(String k, String v, Op op, boolean considerValAsKey) {
225 super(k, v, op, considerValAsKey);
226 CheckParameterUtil.ensureThat(!considerValAsKey, "considerValAsKey is not supported");
227 CheckParameterUtil.ensureThat(SUPPORTED_OPS.contains(op), "Op must be REGEX or NREGEX");
228 this.pattern = Pattern.compile(v);
229 }
230
231 @Override
232 public boolean applies(Environment env) {
233 final String value = env.osm.get(k);
234 if (Op.REGEX.equals(op)) {
235 return value != null && pattern.matcher(value).find();
236 } else if (Op.NREGEX.equals(op)) {
237 return value == null || !pattern.matcher(value).find();
238 } else {
239 throw new IllegalStateException();
240 }
241 }
242 }
243
244 public static class RoleCondition extends Condition {
245 public final String role;
246 public final Op op;
247
248 public RoleCondition(String role, Op op) {
249 this.role = role;
250 this.op = op;
251 }
252
253 @Override
254 public boolean applies(Environment env) {
255 String testRole = env.getRole();
256 if (testRole == null) return false;
257 return op.eval(testRole, role);
258 }
259 }
260
261 public static class IndexCondition extends Condition {
262 public final String index;
263 public final Op op;
264
265 public IndexCondition(String index, Op op) {
266 this.index = index;
267 this.op = op;
268 }
269
270 @Override
271 public boolean applies(Environment env) {
272 if (env.index == null) return false;
273 return op.eval(Integer.toString(env.index + 1), index);
274 }
275 }
276
277 public static enum KeyMatchType {
278 EQ, TRUE, FALSE, REGEX
279 }
280
281 /**
282 * <p>KeyCondition represent one of the following conditions in either the link or the
283 * primitive context:</p>
284 * <pre>
285 * ["a label"] PRIMITIVE: the primitive has a tag "a label"
286 * LINK: the parent is a relation and it has at least one member with the role
287 * "a label" referring to the child
288 *
289 * [!"a label"] PRIMITIVE: the primitive doesn't have a tag "a label"
290 * LINK: the parent is a relation but doesn't have a member with the role
291 * "a label" referring to the child
292 *
293 * ["a label"?] PRIMITIVE: the primitive has a tag "a label" whose value evaluates to a true-value
294 * LINK: not supported
295 *
296 * ["a label"?!] PRIMITIVE: the primitive has a tag "a label" whose value evaluates to a false-value
297 * LINK: not supported
298 * </pre>
299 */
300 public static class KeyCondition extends Condition {
301
302 public final String label;
303 public final boolean negateResult;
304 public final KeyMatchType matchType;
305 public Predicate<String> containsPattern;
306
307 public KeyCondition(String label, boolean negateResult, KeyMatchType matchType){
308 this.label = label;
309 this.negateResult = negateResult;
310 this.matchType = matchType;
311 this.containsPattern = KeyMatchType.REGEX.equals(matchType)
312 ? Predicates.stringContainsPattern(Pattern.compile(label))
313 : null;
314 }
315
316 @Override
317 public boolean applies(Environment e) {
318 switch(e.getContext()) {
319 case PRIMITIVE:
320 if (KeyMatchType.TRUE.equals(matchType))
321 return e.osm.isKeyTrue(label) ^ negateResult;
322 else if (KeyMatchType.FALSE.equals(matchType))
323 return e.osm.isKeyFalse(label) ^ negateResult;
324 else if (KeyMatchType.REGEX.equals(matchType)) {
325 return Utils.exists(e.osm.keySet(), containsPattern) ^ negateResult;
326 } else {
327 return e.osm.hasKey(label) ^ negateResult;
328 }
329 case LINK:
330 Utils.ensure(false, "Illegal state: KeyCondition not supported in LINK context");
331 return false;
332 default: throw new AssertionError();
333 }
334 }
335
336 public Tag asTag() {
337 return new Tag(label);
338 }
339
340 @Override
341 public String toString() {
342 return "[" + (negateResult ? "!" : "") + label + "]";
343 }
344 }
345
346 public static class ClassCondition extends Condition {
347
348 public final String id;
349 public final boolean not;
350
351 public ClassCondition(String id, boolean not) {
352 this.id = id;
353 this.not = not;
354 }
355
356 @Override
357 public boolean applies(Environment env) {
358 return env != null && env.getCascade(env.layer) != null && not ^ env.getCascade(env.layer).containsKey(id);
359 }
360
361 @Override
362 public String toString() {
363 return (not ? "!" : "") + "." + id;
364 }
365 }
366
367 public static class PseudoClassCondition extends Condition {
368
369 public final String id;
370 public final boolean not;
371
372 public PseudoClassCondition(String id, boolean not, Context context) {
373 this.id = id;
374 this.not = not;
375 CheckParameterUtil.ensureThat(!"sameTags".equals(id) || Context.LINK.equals(context), "sameTags only supported in LINK context");
376 }
377
378 @Override
379 public boolean applies(Environment e) {
380 return not ^ appliesImpl(e);
381 }
382
383 public boolean appliesImpl(Environment e) {
384 switch(id) {
385 case "closed":
386 if (e.osm instanceof Way && ((Way) e.osm).isClosed())
387 return true;
388 if (e.osm instanceof Relation && ((Relation) e.osm).isMultipolygon())
389 return true;
390 break;
391 case "modified":
392 return e.osm.isModified() || e.osm.isNewOrUndeleted();
393 case "new":
394 return e.osm.isNew();
395 case "connection":
396 return e.osm instanceof Node && ((Node) e.osm).isConnectionNode();
397 case "tagged":
398 return e.osm.isTagged();
399 case "sameTags":
400 return e.osm.hasSameInterestingTags(Utils.firstNonNull(e.child, e.parent));
401 case "areaStyle":
402 return ElemStyles.hasAreaElemStyle(e.osm, false);
403 case "unconnected":
404 return e.osm instanceof Node && OsmPrimitive.getFilteredList(e.osm.getReferrers(), Way.class).isEmpty();
405 }
406 return false;
407 }
408
409 @Override
410 public String toString() {
411 return ":" + (not ? "!" : "") + id;
412 }
413 }
414
415 public static class ExpressionCondition extends Condition {
416
417 private final Expression e;
418
419 public ExpressionCondition(Expression e) {
420 this.e = e;
421 }
422
423 @Override
424 public boolean applies(Environment env) {
425 Boolean b = Cascade.convertTo(e.evaluate(env), Boolean.class);
426 return b != null && b;
427 }
428
429 @Override
430 public String toString() {
431 return "[" + e + "]";
432 }
433 }
434}
Note: See TracBrowser for help on using the repository browser.