source: josm/trunk/src/org/openstreetmap/josm/gui/mappaint/mapcss/ConditionFactory.java

Last change on this file was 18801, checked in by taylor.smock, 16 months ago

Fix #22832: Code cleanup and some simplification, documentation fixes (patch by gaben)

There should not be any functional changes in this patch; it is intended to do
the following:

  • Simplify and cleanup code (example: Arrays.asList(item) -> Collections.singletonList(item))
  • Fix typos in documentation (which also corrects the documentation to match what actually happens, in some cases)
  • Property svn:eol-style set to native
File size: 37.1 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.EnumSet;
6import java.util.HashMap;
7import java.util.Locale;
8import java.util.Map;
9import java.util.Objects;
10import java.util.Set;
11import java.util.function.BiPredicate;
12import java.util.function.IntFunction;
13import java.util.function.Predicate;
14import java.util.regex.Pattern;
15import java.util.regex.PatternSyntaxException;
16
17import org.openstreetmap.josm.data.osm.INode;
18import org.openstreetmap.josm.data.osm.IPrimitive;
19import org.openstreetmap.josm.data.osm.IRelation;
20import org.openstreetmap.josm.data.osm.IWay;
21import org.openstreetmap.josm.data.osm.Node;
22import org.openstreetmap.josm.data.osm.OsmPrimitive;
23import org.openstreetmap.josm.data.osm.OsmUtils;
24import org.openstreetmap.josm.data.osm.Relation;
25import org.openstreetmap.josm.data.osm.Tag;
26import org.openstreetmap.josm.data.osm.Tagged;
27import org.openstreetmap.josm.data.osm.search.SearchCompiler.InDataSourceArea;
28import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
29import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
30import org.openstreetmap.josm.gui.mappaint.Cascade;
31import org.openstreetmap.josm.gui.mappaint.ElemStyles;
32import org.openstreetmap.josm.gui.mappaint.Environment;
33import org.openstreetmap.josm.gui.mappaint.mapcss.Condition.Context;
34import org.openstreetmap.josm.gui.mappaint.mapcss.Condition.TagCondition;
35import org.openstreetmap.josm.tools.CheckParameterUtil;
36import org.openstreetmap.josm.tools.Utils;
37
38/**
39 * Factory to generate {@link Condition}s.
40 * @since 10837 (Extracted from Condition)
41 */
42public final class ConditionFactory {
43
44 private ConditionFactory() {
45 // Hide default constructor for utils classes
46 }
47
48 /**
49 * Create a new condition that checks the key and the value of the object.
50 * @param k The key.
51 * @param v The reference value
52 * @param op The operation to use when comparing the value
53 * @param context The type of context to use.
54 * @param considerValAsKey whether to consider {@code v} as another key and compare the values of key {@code k} and key {@code v}.
55 * @return The new condition.
56 * @throws MapCSSException if the arguments are incorrect
57 */
58 public static Condition createKeyValueCondition(String k, String v, Op op, Context context, boolean considerValAsKey) {
59 switch (context) {
60 case PRIMITIVE:
61 if (KeyValueRegexpCondition.SUPPORTED_OPS.contains(op) && !considerValAsKey) {
62 try {
63 return new KeyValueRegexpCondition(k, v, op, false);
64 } catch (PatternSyntaxException e) {
65 throw new MapCSSException(e);
66 }
67 }
68 if (!considerValAsKey && op == Op.EQ)
69 return new SimpleKeyValueCondition(k, v);
70 return new KeyValueCondition(k, v, op, considerValAsKey);
71 case LINK:
72 if (considerValAsKey)
73 throw new MapCSSException("''considerValAsKey'' not supported in LINK context");
74 if ("role".equalsIgnoreCase(k))
75 return new RoleCondition(v, op);
76 else if ("index".equalsIgnoreCase(k))
77 return new IndexCondition(v, op);
78 else
79 throw new MapCSSException(
80 MessageFormat.format("Expected key ''role'' or ''index'' in link context. Got ''{0}''.", k));
81
82 default: throw new AssertionError();
83 }
84 }
85
86 /**
87 * Create a condition in which the key and the value need to match a given regexp
88 * @param k The key regexp
89 * @param v The value regexp
90 * @param op The operation to use when comparing the key and the value.
91 * @return The new condition.
92 */
93 public static Condition createRegexpKeyRegexpValueCondition(String k, String v, Op op) {
94 return new RegexpKeyValueRegexpCondition(k, v, op);
95 }
96
97 /**
98 * Creates a condition that checks the given key.
99 * @param k The key to test for
100 * @param not <code>true</code> to invert the match
101 * @param matchType The match type to check for.
102 * @param context The context this rule is found in.
103 * @return the new condition.
104 */
105 public static Condition createKeyCondition(String k, boolean not, KeyMatchType matchType, Context context) {
106 switch (context) {
107 case PRIMITIVE:
108 if (KeyMatchType.REGEX == matchType && k.matches("[A-Za-z0-9:_-]+")) {
109 // optimization: using String.contains avoids allocating a Matcher
110 return new KeyCondition(k, not, KeyMatchType.ANY_CONTAINS);
111 } else if (KeyMatchType.REGEX == matchType && k.matches("\\^[A-Za-z0-9:_-]+")) {
112 // optimization: using String.startsWith avoids allocating a Matcher
113 return new KeyCondition(k.substring(1), not, KeyMatchType.ANY_STARTS_WITH);
114 } else if (KeyMatchType.REGEX == matchType && k.matches("[A-Za-z0-9:_-]+\\$")) {
115 // optimization: using String.endsWith avoids allocating a Matcher
116 return new KeyCondition(k.substring(0, k.length() - 1), not, KeyMatchType.ANY_ENDS_WITH);
117 } else if (matchType == KeyMatchType.REGEX) {
118 return new KeyRegexpCondition(Pattern.compile(k), not);
119 } else {
120 return new KeyCondition(k, not, matchType);
121 }
122 case LINK:
123 if (matchType != null)
124 throw new MapCSSException("Question mark operator ''?'' and regexp match not supported in LINK context");
125 if (not)
126 return new RoleCondition(k, Op.NEQ);
127 else
128 return new RoleCondition(k, Op.EQ);
129
130 default: throw new AssertionError();
131 }
132 }
133
134 /**
135 * Create a new pseudo class condition
136 * @param id The id of the pseudo class
137 * @param not <code>true</code> to invert the condition
138 * @param context The context the class is found in.
139 * @return The new condition
140 */
141 public static PseudoClassCondition createPseudoClassCondition(String id, boolean not, Context context) {
142 return PseudoClassCondition.createPseudoClassCondition(id, not, context);
143 }
144
145 /**
146 * Create a new class condition
147 * @param id The id of the class to match
148 * @param not <code>true</code> to invert the condition
149 * @param context Ignored
150 * @return The new condition
151 */
152 public static ClassCondition createClassCondition(String id, boolean not, Context context) {
153 return new ClassCondition(id, not);
154 }
155
156 /**
157 * Create a new condition that a expression needs to be fulfilled
158 * @param e the expression to check
159 * @param context Ignored
160 * @return The new condition
161 */
162 public static ExpressionCondition createExpressionCondition(Expression e, Context context) {
163 return new ExpressionCondition(e);
164 }
165
166 /**
167 * This is the operation that {@link KeyValueCondition} uses to match.
168 */
169 public enum Op {
170 /** The value equals the given reference. */
171 EQ(Objects::equals),
172 /** The value does not equal the reference. */
173 NEQ(EQ),
174 /** The value is greater than or equal to the given reference value (as float). */
175 GREATER_OR_EQUAL(comparisonResult -> comparisonResult >= 0),
176 /** The value is greater than the given reference value (as float). */
177 GREATER(comparisonResult -> comparisonResult > 0),
178 /** The value is less than or equal to the given reference value (as float). */
179 LESS_OR_EQUAL(comparisonResult -> comparisonResult <= 0),
180 /** The value is less than the given reference value (as float). */
181 LESS(comparisonResult -> comparisonResult < 0),
182 /** The reference is treated as regular expression and the value needs to match it. */
183 REGEX((test, prototype) -> Pattern.compile(prototype).matcher(test).find()),
184 /** The reference is treated as regular expression and the value needs to not match it. */
185 NREGEX(REGEX),
186 /** The reference is treated as a list separated by ';'. Spaces around the ; are ignored.
187 * The value needs to be equal one of the list elements. */
188 ONE_OF((test, prototype) -> OsmUtils.splitMultipleValues(test).anyMatch(prototype::equals)),
189 /** The value needs to begin with the reference string. */
190 BEGINS_WITH(String::startsWith),
191 /** The value needs to end with the reference string. */
192 ENDS_WITH(String::endsWith),
193 /** The value needs to contain the reference string. */
194 CONTAINS(String::contains);
195
196 static final Set<Op> NEGATED_OPS = EnumSet.of(NEQ, NREGEX);
197
198 @SuppressWarnings("ImmutableEnumChecker")
199 private final BiPredicate<String, String> function;
200
201 private final boolean negated;
202
203 /**
204 * Create a new string operation.
205 * @param func The function to apply during {@link #eval(String, String)}.
206 */
207 Op(BiPredicate<String, String> func) {
208 this.function = func;
209 negated = false;
210 }
211
212 /**
213 * Create a new float operation that compares two float values
214 * @param comparatorResult A function to map the result of the comparison
215 */
216 Op(IntFunction<Boolean> comparatorResult) {
217 this.function = (test, prototype) -> {
218 float testFloat;
219 try {
220 testFloat = Float.parseFloat(test);
221 } catch (NumberFormatException e) {
222 return Boolean.FALSE;
223 }
224 float prototypeFloat = Float.parseFloat(prototype);
225
226 int res = Float.compare(testFloat, prototypeFloat);
227 return comparatorResult.apply(res);
228 };
229 negated = false;
230 }
231
232 /**
233 * Create a new Op by negating an other op.
234 * @param negate inverse operation
235 */
236 Op(Op negate) {
237 this.function = (a, b) -> !negate.function.test(a, b);
238 negated = true;
239 }
240
241 /**
242 * Evaluates a value against a reference string.
243 * @param testString The value. May be <code>null</code>
244 * @param prototypeString The reference string-
245 * @return <code>true</code> if and only if this operation matches for the given value/reference pair.
246 */
247 public boolean eval(String testString, String prototypeString) {
248 if (testString == null)
249 return negated;
250 else
251 return function.test(testString, prototypeString);
252 }
253 }
254
255 /**
256 * Most common case of a KeyValueCondition, this is the basic key=value case.
257 *
258 * Extra class for performance reasons.
259 */
260 public static class SimpleKeyValueCondition implements TagCondition {
261 /**
262 * The key to search for.
263 */
264 public final String k;
265 /**
266 * The value to search for.
267 */
268 public final String v;
269
270 /**
271 * Create a new SimpleKeyValueCondition.
272 * @param k The key
273 * @param v The value.
274 */
275 public SimpleKeyValueCondition(String k, String v) {
276 this.k = k.intern();
277 this.v = v.intern();
278 }
279
280 @Override
281 public boolean applies(Tagged osm) {
282 return v.equals(osm.get(k));
283 }
284
285 @Override
286 public Tag asTag(Tagged primitive) {
287 return new Tag(k, v);
288 }
289
290 @Override
291 public String toString() {
292 return '[' + k + '=' + v + ']';
293 }
294
295 }
296
297 /**
298 * <p>Represents a key/value condition which is either applied to a primitive.</p>
299 *
300 */
301 public static class KeyValueCondition implements TagCondition {
302 /**
303 * The key to search for.
304 */
305 public final String k;
306 /**
307 * The value to search for.
308 */
309 public final String v;
310 /**
311 * The key/value match operation.
312 */
313 public final Op op;
314 /**
315 * If this flag is set, {@link #v} is treated as a key and the value is the value set for that key.
316 */
317 public final boolean considerValAsKey;
318
319 /**
320 * <p>Creates a key/value-condition.</p>
321 *
322 * @param k the key
323 * @param v the value
324 * @param op the operation
325 * @param considerValAsKey whether to consider {@code v} as another key and compare the values of key {@code k} and key {@code v}.
326 */
327 public KeyValueCondition(String k, String v, Op op, boolean considerValAsKey) {
328 this.k = k.intern();
329 this.v = v.intern();
330 this.op = op;
331 this.considerValAsKey = considerValAsKey;
332 }
333
334 /**
335 * Determines if this condition requires an exact key match.
336 * @return {@code true} if this condition requires an exact key match.
337 * @since 14801
338 */
339 public boolean requiresExactKeyMatch() {
340 return !Op.NEGATED_OPS.contains(op);
341 }
342
343 @Override
344 public boolean applies(Tagged osm) {
345 return op.eval(osm.get(k), considerValAsKey ? osm.get(v) : v);
346 }
347
348 @Override
349 public Tag asTag(Tagged primitive) {
350 return new Tag(k, v);
351 }
352
353 @Override
354 public String toString() {
355 return '[' + k + '\'' + op + '\'' + v + ']';
356 }
357 }
358
359 /**
360 * This condition requires a fixed key to match a given regexp
361 */
362 public static class KeyValueRegexpCondition extends KeyValueCondition {
363 protected static final Set<Op> SUPPORTED_OPS = EnumSet.of(Op.REGEX, Op.NREGEX);
364
365 final Pattern pattern;
366
367 /**
368 * Constructs a new {@code KeyValueRegexpCondition}.
369 * @param k key
370 * @param v value
371 * @param op operation
372 * @param considerValAsKey must be false
373 * @throws PatternSyntaxException if the value syntax is invalid
374 */
375 public KeyValueRegexpCondition(String k, String v, Op op, boolean considerValAsKey) {
376 super(k, v, op, considerValAsKey); /* value is needed in validator messages */
377 CheckParameterUtil.ensureThat(!considerValAsKey, "considerValAsKey is not supported");
378 CheckParameterUtil.ensureThat(SUPPORTED_OPS.contains(op), "Op must be REGEX or NREGEX");
379 this.pattern = Pattern.compile(v);
380 }
381
382 protected boolean matches(Tagged osm) {
383 final String value = osm.get(k);
384 return value != null && pattern.matcher(value).find();
385 }
386
387 @Override
388 public boolean applies(Tagged osm) {
389 if (Op.REGEX == op) {
390 return matches(osm);
391 } else if (Op.NREGEX == op) {
392 return !matches(osm);
393 } else {
394 throw new IllegalStateException();
395 }
396 }
397 }
398
399 /**
400 * A condition that checks that a key with the matching pattern has a value with the matching pattern.
401 */
402 public static class RegexpKeyValueRegexpCondition extends KeyValueRegexpCondition {
403
404 final Pattern keyPattern;
405
406 /**
407 * Create a condition in which the key and the value need to match a given regexp
408 * @param k The key regexp
409 * @param v The value regexp
410 * @param op The operation to use when comparing the key and the value.
411 */
412 public RegexpKeyValueRegexpCondition(String k, String v, Op op) {
413 super(k, v, op, false);
414 this.keyPattern = Pattern.compile(k);
415 }
416
417 @Override
418 public boolean requiresExactKeyMatch() {
419 return false;
420 }
421
422 @Override
423 protected boolean matches(Tagged osm) {
424 return osm.getKeys().entrySet().stream()
425 .anyMatch(kv -> keyPattern.matcher(kv.getKey()).find() && pattern.matcher(kv.getValue()).find());
426 }
427 }
428
429 /**
430 * Role condition.
431 */
432 public static class RoleCondition implements Condition {
433 final String role;
434 final Op op;
435
436 /**
437 * Constructs a new {@code RoleCondition}.
438 * @param role role
439 * @param op operation
440 */
441 public RoleCondition(String role, Op op) {
442 this.role = role;
443 this.op = op;
444 }
445
446 @Override
447 public boolean applies(Environment env) {
448 String testRole = env.getRole();
449 if (testRole == null) return false;
450 return op.eval(testRole, role);
451 }
452 }
453
454 /**
455 * Index condition.
456 */
457 public static class IndexCondition implements Condition {
458 final String index;
459 final Op op;
460 final boolean isFirstOrLast;
461
462 /**
463 * Constructs a new {@code IndexCondition}.
464 * @param index index
465 * @param op operation
466 */
467 public IndexCondition(String index, Op op) {
468 this.index = index;
469 this.op = op;
470 isFirstOrLast = op == Op.EQ && ("1".equals(index) || "-1".equals(index));
471 }
472
473 @Override
474 public boolean applies(Environment env) {
475 if (env.index == null) return false;
476 if (index.startsWith("-")) {
477 return env.count != null && op.eval(Integer.toString(env.index - env.count), index);
478 } else {
479 return op.eval(Integer.toString(env.index + 1), index);
480 }
481 }
482 }
483
484 /**
485 * This defines how {@link KeyCondition} matches a given key.
486 */
487 public enum KeyMatchType {
488 /**
489 * The key needs to be equal to the given label.
490 */
491 EQ,
492 /**
493 * The key needs to have a true value (yes, ...)
494 * @see OsmUtils#isTrue(String)
495 */
496 TRUE,
497 /**
498 * The key needs to have a false value (no, ...)
499 * @see OsmUtils#isFalse(String)
500 */
501 FALSE,
502 /**
503 * The key needs to match the given regular expression.
504 */
505 REGEX,
506 /**
507 * The key needs to contain the given label as substring.
508 */
509 ANY_CONTAINS,
510 /**
511 * The key needs to start with the given label.
512 */
513 ANY_STARTS_WITH,
514 /**
515 * The key needs to end with the given label.
516 */
517 ANY_ENDS_WITH,
518 }
519
520 /**
521 * <p>KeyCondition represent one of the following conditions in either the link or the
522 * primitive context:</p>
523 * <pre>
524 * ["a label"] PRIMITIVE: the primitive has a tag "a label"
525 * LINK: the parent is a relation and it has at least one member with the role
526 * "a label" referring to the child
527 *
528 * [!"a label"] PRIMITIVE: the primitive doesn't have a tag "a label"
529 * LINK: the parent is a relation but doesn't have a member with the role
530 * "a label" referring to the child
531 *
532 * ["a label"?] PRIMITIVE: the primitive has a tag "a label" whose value evaluates to a true-value
533 * LINK: not supported
534 *
535 * ["a label"?!] PRIMITIVE: the primitive has a tag "a label" whose value evaluates to a false-value
536 * LINK: not supported
537 * </pre>
538 * @see KeyRegexpCondition
539 */
540 public static class KeyCondition implements TagCondition {
541
542 /**
543 * The key name.
544 */
545 public final String label;
546 /**
547 * If we should negate the result of the match.
548 */
549 public final boolean negateResult;
550 /**
551 * Describes how to match the label against the key.
552 * @see KeyMatchType
553 */
554 public final KeyMatchType matchType;
555
556 /**
557 * Creates a new KeyCondition
558 * @param label The key name (or regexp) to use.
559 * @param negateResult If we should negate the result.,
560 * @param matchType The match type.
561 */
562 public KeyCondition(String label, boolean negateResult, KeyMatchType matchType) {
563 CheckParameterUtil.ensureThat(matchType != KeyMatchType.REGEX, "Use KeyPatternCondition");
564 this.label = label;
565 this.negateResult = negateResult;
566 this.matchType = matchType == null ? KeyMatchType.EQ : matchType;
567 }
568
569 @Override
570 public boolean applies(Tagged osm) {
571 switch (matchType) {
572 case TRUE:
573 return osm.isKeyTrue(label) ^ negateResult;
574 case FALSE:
575 return osm.isKeyFalse(label) ^ negateResult;
576 case ANY_CONTAINS:
577 case ANY_STARTS_WITH:
578 case ANY_ENDS_WITH:
579 return osm.keys().anyMatch(keyPredicate()) ^ negateResult;
580 default:
581 return osm.hasKey(label) ^ negateResult;
582 }
583 }
584
585 private Predicate<String> keyPredicate() {
586 switch (matchType) {
587 case ANY_CONTAINS:
588 return key -> key.contains(label);
589 case ANY_STARTS_WITH:
590 return key -> key.startsWith(label);
591 case ANY_ENDS_WITH:
592 return key -> key.endsWith(label);
593 default:
594 return null;
595 }
596 }
597
598 /**
599 * Get the matched key and the corresponding value.
600 * <p>
601 * WARNING: This ignores {@link #negateResult}.
602 * @param p The primitive to get the value from.
603 * @return The tag.
604 */
605 @Override
606 public Tag asTag(Tagged p) {
607 String key = label;
608 Predicate<String> keyPredicate = keyPredicate();
609 if (keyPredicate != null) {
610 key = p.keys().filter(keyPredicate).findAny().orElse(key);
611 }
612 return new Tag(key, p.get(key));
613 }
614
615 @Override
616 public String toString() {
617 return '[' + (negateResult ? "!" : "") + label + ']';
618 }
619 }
620
621 /**
622 * KeyPatternCondition represents a conditions matching keys based on a pattern.
623 */
624 public static class KeyRegexpCondition implements TagCondition {
625
626 /**
627 * A predicate used to match a the regexp against the key. Only used if the match type is regexp.
628 */
629 public final Pattern pattern;
630 /**
631 * If we should negate the result of the match.
632 */
633 public final boolean negateResult;
634
635 /**
636 * Creates a new KeyPatternCondition
637 * @param pattern The regular expression for matching keys.
638 * @param negateResult If we should negate the result.
639 */
640 public KeyRegexpCondition(Pattern pattern, boolean negateResult) {
641 this.negateResult = negateResult;
642 this.pattern = pattern;
643 }
644
645 @Override
646 public boolean applies(Tagged osm) {
647 boolean matches = osm.hasKeys() && osm.keys().anyMatch(pattern.asPredicate());
648 return matches ^ negateResult;
649 }
650
651 /**
652 * Get the matched key and the corresponding value.
653 * <p>
654 * WARNING: This ignores {@link #negateResult}.
655 * <p>
656 * WARNING: For regexp, the regular expression is returned instead of a key if the match failed.
657 * @param p The primitive to get the value from.
658 * @return The tag.
659 */
660 @Override
661 public Tag asTag(Tagged p) {
662 String key = p.keys().filter(pattern.asPredicate()).findAny().orElse(pattern.pattern());
663 return new Tag(key, p.get(key));
664 }
665
666 @Override
667 public String toString() {
668 return '[' + (negateResult ? "!" : "") + pattern + ']';
669 }
670 }
671
672 /**
673 * Class condition.
674 */
675 public static class ClassCondition implements Condition {
676
677 /** Class identifier */
678 public final String id;
679 final boolean not;
680
681 /**
682 * Constructs a new {@code ClassCondition}.
683 * @param id id
684 * @param not negation or not
685 */
686 public ClassCondition(String id, boolean not) {
687 this.id = id;
688 this.not = not;
689 }
690
691 @Override
692 public boolean applies(Environment env) {
693 Cascade cascade = env.getCascade();
694 return cascade != null && (not ^ cascade.containsKey(id));
695 }
696
697 @Override
698 public String toString() {
699 return (not ? "!" : "") + '.' + id;
700 }
701 }
702
703 /**
704 * Like <a href="http://www.w3.org/TR/css3-selectors/#pseudo-classes">CSS pseudo classes</a>, MapCSS pseudo classes
705 * are written in lower case with dashes between words.
706 */
707 public static final class PseudoClasses {
708
709 private PseudoClasses() {
710 // Hide default constructor for utilities classes
711 }
712
713 /**
714 * {@code :closed} tests whether the way is closed or the relation is a closed multipolygon
715 * @param e MapCSS environment
716 * @return {@code true} if the way is closed or the relation is a closed multipolygon
717 */
718 static boolean closed(Environment e) {
719 if (e.osm instanceof IWay<?> && ((IWay<?>) e.osm).isClosed())
720 return true;
721 return e.osm instanceof IRelation<?> && e.osm.isMultipolygon();
722 }
723
724 /**
725 * {@code :modified} tests whether the object has been modified.
726 * @param e MapCSS environment
727 * @return {@code true} if the object has been modified
728 * @see IPrimitive#isModified()
729 */
730 static boolean modified(Environment e) {
731 return e.osm.isModified() || e.osm.isNewOrUndeleted();
732 }
733
734 /**
735 * {@code :new} tests whether the object is new.
736 * @param e MapCSS environment
737 * @return {@code true} if the object is new
738 * @see IPrimitive#isNew()
739 */
740 static boolean _new(Environment e) {
741 return e.osm.isNew();
742 }
743
744 /**
745 * {@code :connection} tests whether the object is a connection node.
746 * @param e MapCSS environment
747 * @return {@code true} if the object is a connection node
748 * @see Node#isConnectionNode()
749 */
750 static boolean connection(Environment e) {
751 return e.osm instanceof INode && e.osm.getDataSet() != null && ((INode) e.osm).isConnectionNode();
752 }
753
754 /**
755 * {@code :tagged} tests whether the object is tagged.
756 * @param e MapCSS environment
757 * @return {@code true} if the object is tagged
758 * @see IPrimitive#isTagged()
759 */
760 static boolean tagged(Environment e) {
761 return e.osm.isTagged();
762 }
763
764 /**
765 * {@code :same-tags} tests whether the object has the same tags as its child/parent.
766 * @param e MapCSS environment
767 * @return {@code true} if the object has the same tags as its child/parent
768 * @see IPrimitive#hasSameInterestingTags(IPrimitive)
769 */
770 static boolean sameTags(Environment e) {
771 return e.osm.hasSameInterestingTags(Utils.firstNonNull(e.child, e.parent));
772 }
773
774 /**
775 * {@code :area-style} tests whether the object has an area style. This is useful for validators.
776 * @param e MapCSS environment
777 * @return {@code true} if the object has an area style
778 * @see ElemStyles#hasAreaElemStyle(IPrimitive, boolean)
779 */
780 static boolean areaStyle(Environment e) {
781 // only for validator
782 return ElemStyles.hasAreaElemStyle(e.osm, false);
783 }
784
785 /**
786 * {@code :unconnected} tests whether the object is an unconnected node.
787 * @param e MapCSS environment
788 * @return {@code true} if the object is an unconnected node
789 */
790 static boolean unconnected(Environment e) {
791 return e.osm instanceof Node && ((Node) e.osm).getParentWays().isEmpty();
792 }
793
794 /**
795 * {@code :righthandtraffic} checks if there is right-hand traffic at the current location.
796 * @param e MapCSS environment
797 * @return {@code true} if there is right-hand traffic at the current location
798 * @see Functions#is_right_hand_traffic(Environment)
799 */
800 static boolean righthandtraffic(Environment e) {
801 return Functions.is_right_hand_traffic(e);
802 }
803
804 /**
805 * {@code :clockwise} whether the way is closed and oriented clockwise,
806 * or non-closed and the 1st, 2nd and last node are in clockwise order.
807 * @param e MapCSS environment
808 * @return {@code true} if the way clockwise
809 * @see Functions#is_clockwise(Environment)
810 */
811 static boolean clockwise(Environment e) {
812 return Functions.is_clockwise(e);
813 }
814
815 /**
816 * {@code :anticlockwise} whether the way is closed and oriented anticlockwise,
817 * or non-closed and the 1st, 2nd and last node are in anticlockwise order.
818 * @param e MapCSS environment
819 * @return {@code true} if the way clockwise
820 * @see Functions#is_anticlockwise(Environment)
821 */
822 static boolean anticlockwise(Environment e) {
823 return Functions.is_anticlockwise(e);
824 }
825
826 /**
827 * {@code :unclosed-multipolygon} tests whether the object is an unclosed multipolygon.
828 * @param e MapCSS environment
829 * @return {@code true} if the object is an unclosed multipolygon
830 */
831 static boolean unclosed_multipolygon(Environment e) {
832 return e.osm instanceof Relation && e.osm.isMultipolygon() &&
833 !e.osm.isIncomplete() && !((Relation) e.osm).hasIncompleteMembers() &&
834 !MultipolygonCache.getInstance().get((Relation) e.osm).getOpenEnds().isEmpty();
835 }
836
837 private static final Predicate<OsmPrimitive> IN_DOWNLOADED_AREA = new InDataSourceArea(false);
838
839 /**
840 * {@code :in-downloaded-area} tests whether the object is within source area ("downloaded area").
841 * @param e MapCSS environment
842 * @return {@code true} if the object is within source area ("downloaded area")
843 * @see InDataSourceArea
844 */
845 static boolean inDownloadedArea(Environment e) {
846 return e.osm instanceof OsmPrimitive && IN_DOWNLOADED_AREA.test((OsmPrimitive) e.osm);
847 }
848
849 /**
850 * {@code :completely_downloaded} tests whether the object is completely downloaded
851 * @param e MapCSS environment
852 * @return {@code true} if the object is completely downloaded
853 */
854 static boolean completely_downloaded(Environment e) {
855 if (e.osm instanceof IRelation<?>) {
856 return !((IRelation<?>) e.osm).hasIncompleteMembers();
857 } else if (e.osm instanceof IWay<?>) {
858 return !((IWay<?>) e.osm).hasIncompleteNodes();
859 } else if (e.osm instanceof INode) {
860 return ((INode) e.osm).isLatLonKnown();
861 } else {
862 return true;
863 }
864 }
865
866 static boolean closed2(Environment e) {
867 if (e.osm instanceof IWay<?> && ((IWay<?>) e.osm).isClosed())
868 return true;
869 if (e.osm instanceof Relation && e.osm.isMultipolygon()) {
870 Multipolygon multipolygon = MultipolygonCache.getInstance().get((Relation) e.osm);
871 return multipolygon != null && multipolygon.getOpenEnds().isEmpty();
872 }
873 return false;
874 }
875
876 /**
877 * {@code :selected} tests whether the object is selected in the editor
878 * @param e MapCSS environment
879 * @return {@code true} if the object is selected
880 */
881 static boolean selected(Environment e) {
882 if (e.mc != null) {
883 e.getCascade().setDefaultSelectedHandling(false);
884 }
885 return e.osm.isSelected();
886 }
887
888 /**
889 * {@code :highlighted} tests whether the object is highlighted (i.e. is hovered over)
890 * @param e The MapCSS environment
891 * @return {@code true} if the object is highlighted
892 * @see IPrimitive#isHighlighted
893 * @since 17862
894 */
895 static boolean highlighted(Environment e) {
896 return e.osm.isHighlighted();
897 }
898 }
899
900 /**
901 * Pseudo class condition.
902 */
903 public static class PseudoClassCondition implements Condition {
904
905 static final Map<String, PseudoClassCondition> CONDITION_MAP = new HashMap<>();
906
907 static {
908 PseudoClassCondition.register("anticlockwise", PseudoClasses::anticlockwise);
909 PseudoClassCondition.register("areaStyle", PseudoClasses::areaStyle);
910 PseudoClassCondition.register("clockwise", PseudoClasses::clockwise);
911 PseudoClassCondition.register("closed", PseudoClasses::closed);
912 PseudoClassCondition.register("closed2", PseudoClasses::closed2);
913 PseudoClassCondition.register("completely_downloaded", PseudoClasses::completely_downloaded);
914 PseudoClassCondition.register("connection", PseudoClasses::connection);
915 PseudoClassCondition.register("highlighted", PseudoClasses::highlighted);
916 PseudoClassCondition.register("inDownloadedArea", PseudoClasses::inDownloadedArea);
917 PseudoClassCondition.register("modified", PseudoClasses::modified);
918 PseudoClassCondition.register("new", PseudoClasses::_new);
919 PseudoClassCondition.register("righthandtraffic", PseudoClasses::righthandtraffic);
920 PseudoClassCondition.register("sameTags", PseudoClasses::sameTags);
921 PseudoClassCondition.register("selected", PseudoClasses::selected);
922 PseudoClassCondition.register("tagged", PseudoClasses::tagged);
923 PseudoClassCondition.register("unclosed_multipolygon", PseudoClasses::unclosed_multipolygon);
924 PseudoClassCondition.register("unconnected", PseudoClasses::unconnected);
925 }
926
927 private static void register(String name, Predicate<Environment> predicate) {
928 CONDITION_MAP.put(clean(name), new PseudoClassCondition(":" + name, predicate));
929 CONDITION_MAP.put("!" + clean(name), new PseudoClassCondition("!:" + name, predicate.negate()));
930 }
931
932 private final String name;
933 private final Predicate<Environment> predicate;
934
935 protected PseudoClassCondition(String name, Predicate<Environment> predicate) {
936 this.name = name;
937 this.predicate = predicate;
938 }
939
940 /**
941 * Create a new pseudo class condition
942 * @param id The id of the pseudo class
943 * @param not <code>true</code> to invert the condition
944 * @param context The context the class is found in.
945 * @return The new condition
946 */
947 public static PseudoClassCondition createPseudoClassCondition(String id, boolean not, Context context) {
948 CheckParameterUtil.ensureThat(!"sameTags".equals(id) || Context.LINK == context, "sameTags only supported in LINK context");
949 if ("open_end".equals(id)) {
950 return new OpenEndPseudoClassCondition(not);
951 }
952 String cleanId = not ? clean("!" + id) : clean(id);
953 PseudoClassCondition condition = CONDITION_MAP.get(cleanId);
954 if (condition != null) {
955 return condition;
956 }
957 throw new MapCSSException("Invalid pseudo class specified: " + id);
958 }
959
960 private static String clean(String id) {
961 // for backwards compatibility, consider :sameTags == :same-tags == :same_tags (#11150)
962 return id.toLowerCase(Locale.ROOT).replaceAll("[-_]", "");
963 }
964
965 @Override
966 public boolean applies(Environment e) {
967 return predicate.test(e);
968 }
969
970 @Override
971 public String toString() {
972 return name;
973 }
974 }
975
976 /**
977 * Open end pseudo class condition.
978 */
979 public static class OpenEndPseudoClassCondition extends PseudoClassCondition {
980 final boolean not;
981 /**
982 * Constructs a new {@code OpenEndPseudoClassCondition}.
983 * @param not negation or not
984 */
985 public OpenEndPseudoClassCondition(boolean not) {
986 super("open_end", null);
987 this.not = not;
988 }
989
990 @Override
991 public boolean applies(Environment e) {
992 return !not;
993 }
994 }
995
996 /**
997 * A condition that is fulfilled whenever the expression is evaluated to be true.
998 */
999 public static class ExpressionCondition implements Condition {
1000
1001 final Expression e;
1002
1003 /**
1004 * Constructs a new {@code ExpressionFactory}
1005 * @param e expression
1006 */
1007 public ExpressionCondition(Expression e) {
1008 this.e = e;
1009 }
1010
1011 /**
1012 * Returns the expression.
1013 * @return the expression
1014 * @since 14484
1015 */
1016 public final Expression getExpression() {
1017 return e;
1018 }
1019
1020 @Override
1021 public boolean applies(Environment env) {
1022 Boolean b = Cascade.convertTo(e.evaluate(env), Boolean.class);
1023 return b != null && b;
1024 }
1025
1026 @Override
1027 public String toString() {
1028 return '[' + e.toString() + ']';
1029 }
1030 }
1031}
Note: See TracBrowser for help on using the repository browser.