001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.mappaint.mapcss;
003
004import java.lang.reflect.Method;
005import java.text.MessageFormat;
006import java.util.Arrays;
007import java.util.EnumSet;
008import java.util.Map;
009import java.util.Objects;
010import java.util.Set;
011import java.util.function.BiPredicate;
012import java.util.function.IntFunction;
013import java.util.function.Predicate;
014import java.util.regex.Pattern;
015import java.util.regex.PatternSyntaxException;
016
017import org.openstreetmap.josm.data.osm.INode;
018import org.openstreetmap.josm.data.osm.IPrimitive;
019import org.openstreetmap.josm.data.osm.IRelation;
020import org.openstreetmap.josm.data.osm.IWay;
021import org.openstreetmap.josm.data.osm.Node;
022import org.openstreetmap.josm.data.osm.OsmPrimitive;
023import org.openstreetmap.josm.data.osm.OsmUtils;
024import org.openstreetmap.josm.data.osm.Relation;
025import org.openstreetmap.josm.data.osm.Tag;
026import org.openstreetmap.josm.data.osm.search.SearchCompiler.InDataSourceArea;
027import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
028import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
029import org.openstreetmap.josm.gui.mappaint.Cascade;
030import org.openstreetmap.josm.gui.mappaint.ElemStyles;
031import org.openstreetmap.josm.gui.mappaint.Environment;
032import org.openstreetmap.josm.gui.mappaint.mapcss.Condition.Context;
033import org.openstreetmap.josm.gui.mappaint.mapcss.Condition.ToTagConvertable;
034import org.openstreetmap.josm.tools.CheckParameterUtil;
035import org.openstreetmap.josm.tools.JosmRuntimeException;
036import org.openstreetmap.josm.tools.Utils;
037
038/**
039 * Factory to generate {@link Condition}s.
040 * @since 10837 (Extracted from Condition)
041 */
042public final class ConditionFactory {
043
044    private ConditionFactory() {
045        // Hide default constructor for utils classes
046    }
047
048    /**
049     * Create a new condition that checks the key and the value of the object.
050     * @param k The key.
051     * @param v The reference value
052     * @param op The operation to use when comparing the value
053     * @param context The type of context to use.
054     * @param considerValAsKey whether to consider {@code v} as another key and compare the values of key {@code k} and key {@code v}.
055     * @return The new condition.
056     * @throws MapCSSException if the arguments are incorrect
057     */
058    public static Condition createKeyValueCondition(String k, String v, Op op, Context context, boolean considerValAsKey) {
059        switch (context) {
060        case PRIMITIVE:
061            if (KeyValueRegexpCondition.SUPPORTED_OPS.contains(op) && !considerValAsKey) {
062                try {
063                    return new KeyValueRegexpCondition(k, v, op, false);
064                } catch (PatternSyntaxException e) {
065                    throw new MapCSSException(e);
066                }
067            }
068            if (!considerValAsKey && op == Op.EQ)
069                return new SimpleKeyValueCondition(k, v);
070            return new KeyValueCondition(k, v, op, considerValAsKey);
071        case LINK:
072            if (considerValAsKey)
073                throw new MapCSSException("''considerValAsKey'' not supported in LINK context");
074            if ("role".equalsIgnoreCase(k))
075                return new RoleCondition(v, op);
076            else if ("index".equalsIgnoreCase(k))
077                return new IndexCondition(v, op);
078            else
079                throw new MapCSSException(
080                        MessageFormat.format("Expected key ''role'' or ''index'' in link context. Got ''{0}''.", k));
081
082        default: throw new AssertionError();
083        }
084    }
085
086    /**
087     * Create a condition in which the key and the value need to match a given regexp
088     * @param k The key regexp
089     * @param v The value regexp
090     * @param op The operation to use when comparing the key and the value.
091     * @return The new condition.
092     */
093    public static Condition createRegexpKeyRegexpValueCondition(String k, String v, Op op) {
094        return new RegexpKeyValueRegexpCondition(k, v, op);
095    }
096
097    /**
098     * Creates a condition that checks the given key.
099     * @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            return new KeyCondition(k, not, matchType);
109        case LINK:
110            if (matchType != null)
111                throw new MapCSSException("Question mark operator ''?'' and regexp match not supported in LINK context");
112            if (not)
113                return new RoleCondition(k, Op.NEQ);
114            else
115                return new RoleCondition(k, Op.EQ);
116
117        default: throw new AssertionError();
118        }
119    }
120
121    /**
122     * Create a new pseudo class condition
123     * @param id The id of the pseudo class
124     * @param not <code>true</code> to invert the condition
125     * @param context The context the class is found in.
126     * @return The new condition
127     */
128    public static PseudoClassCondition createPseudoClassCondition(String id, boolean not, Context context) {
129        return PseudoClassCondition.createPseudoClassCondition(id, not, context);
130    }
131
132    /**
133     * Create a new class condition
134     * @param id The id of the class to match
135     * @param not <code>true</code> to invert the condition
136     * @param context Ignored
137     * @return The new condition
138     */
139    public static ClassCondition createClassCondition(String id, boolean not, Context context) {
140        return new ClassCondition(id, not);
141    }
142
143    /**
144     * Create a new condition that a expression needs to be fulfilled
145     * @param e the expression to check
146     * @param context Ignored
147     * @return The new condition
148     */
149    public static ExpressionCondition createExpressionCondition(Expression e, Context context) {
150        return new ExpressionCondition(e);
151    }
152
153    /**
154     * This is the operation that {@link KeyValueCondition} uses to match.
155     */
156    public enum Op {
157        /** The value equals the given reference. */
158        EQ(Objects::equals),
159        /** The value does not equal the reference. */
160        NEQ(EQ),
161        /** The value is greater than or equal to the given reference value (as float). */
162        GREATER_OR_EQUAL(comparisonResult -> comparisonResult >= 0),
163        /** The value is greater than the given reference value (as float). */
164        GREATER(comparisonResult -> comparisonResult > 0),
165        /** The value is less than or equal to the given reference value (as float). */
166        LESS_OR_EQUAL(comparisonResult -> comparisonResult <= 0),
167        /** The value is less than the given reference value (as float). */
168        LESS(comparisonResult -> comparisonResult < 0),
169        /** The reference is treated as regular expression and the value needs to match it. */
170        REGEX((test, prototype) -> Pattern.compile(prototype).matcher(test).find()),
171        /** The reference is treated as regular expression and the value needs to not match it. */
172        NREGEX(REGEX),
173        /** The reference is treated as a list separated by ';'. Spaces around the ; are ignored.
174         *  The value needs to be equal one of the list elements. */
175        ONE_OF((test, prototype) -> Arrays.asList(test.split("\\s*;\\s*")).contains(prototype)),
176        /** The value needs to begin with the reference string. */
177        BEGINS_WITH(String::startsWith),
178        /** The value needs to end with the reference string. */
179        ENDS_WITH(String::endsWith),
180        /** The value needs to contain the reference string. */
181        CONTAINS(String::contains);
182
183        static final Set<Op> NEGATED_OPS = EnumSet.of(NEQ, NREGEX);
184
185        private final BiPredicate<String, String> function;
186
187        private final boolean negated;
188
189        /**
190         * Create a new string operation.
191         * @param func The function to apply during {@link #eval(String, String)}.
192         */
193        Op(BiPredicate<String, String> func) {
194            this.function = func;
195            negated = false;
196        }
197
198        /**
199         * Create a new float operation that compares two float values
200         * @param comparatorResult A function to mapt the result of the comparison
201         */
202        Op(IntFunction<Boolean> comparatorResult) {
203            this.function = (test, prototype) -> {
204                float testFloat;
205                try {
206                    testFloat = Float.parseFloat(test);
207                } catch (NumberFormatException e) {
208                    return Boolean.FALSE;
209                }
210                float prototypeFloat = Float.parseFloat(prototype);
211
212                int res = Float.compare(testFloat, prototypeFloat);
213                return comparatorResult.apply(res);
214            };
215            negated = false;
216        }
217
218        /**
219         * Create a new Op by negating an other op.
220         * @param negate inverse operation
221         */
222        Op(Op negate) {
223            this.function = (a, b) -> !negate.function.test(a, b);
224            negated = true;
225        }
226
227        /**
228         * Evaluates a value against a reference string.
229         * @param testString The value. May be <code>null</code>
230         * @param prototypeString The reference string-
231         * @return <code>true</code> if and only if this operation matches for the given value/reference pair.
232         */
233        public boolean eval(String testString, String prototypeString) {
234            if (testString == null)
235                return negated;
236            else
237                return function.test(testString, prototypeString);
238        }
239    }
240
241    /**
242     * Most common case of a KeyValueCondition, this is the basic key=value case.
243     *
244     * Extra class for performance reasons.
245     */
246    public static class SimpleKeyValueCondition implements Condition, ToTagConvertable {
247        /**
248         * The key to search for.
249         */
250        public final String k;
251        /**
252         * The value to search for.
253         */
254        public final String v;
255
256        /**
257         * Create a new SimpleKeyValueCondition.
258         * @param k The key
259         * @param v The value.
260         */
261        public SimpleKeyValueCondition(String k, String v) {
262            this.k = k;
263            this.v = v;
264        }
265
266        @Override
267        public boolean applies(Environment e) {
268            return v.equals(e.osm.get(k));
269        }
270
271        @Override
272        public Tag asTag(OsmPrimitive primitive) {
273            return new Tag(k, v);
274        }
275
276        @Override
277        public String toString() {
278            return '[' + k + '=' + v + ']';
279        }
280
281    }
282
283    /**
284     * <p>Represents a key/value condition which is either applied to a primitive.</p>
285     *
286     */
287    public static class KeyValueCondition implements Condition, ToTagConvertable {
288        /**
289         * The key to search for.
290         */
291        public final String k;
292        /**
293         * The value to search for.
294         */
295        public final String v;
296        /**
297         * The key/value match operation.
298         */
299        public final Op op;
300        /**
301         * If this flag is set, {@link #v} is treated as a key and the value is the value set for that key.
302         */
303        public final boolean considerValAsKey;
304
305        /**
306         * <p>Creates a key/value-condition.</p>
307         *
308         * @param k the key
309         * @param v the value
310         * @param op the operation
311         * @param considerValAsKey whether to consider {@code v} as another key and compare the values of key {@code k} and key {@code v}.
312         */
313        public KeyValueCondition(String k, String v, Op op, boolean considerValAsKey) {
314            this.k = k;
315            this.v = v;
316            this.op = op;
317            this.considerValAsKey = considerValAsKey;
318        }
319
320        /**
321         * Determines if this condition requires an exact key match.
322         * @return {@code true} if this condition requires an exact key match.
323         * @since 14801
324         */
325        public boolean requiresExactKeyMatch() {
326            return !Op.NEGATED_OPS.contains(op);
327        }
328
329        @Override
330        public boolean applies(Environment env) {
331            return op.eval(env.osm.get(k), considerValAsKey ? env.osm.get(v) : v);
332        }
333
334        @Override
335        public Tag asTag(OsmPrimitive primitive) {
336            return new Tag(k, v);
337        }
338
339        @Override
340        public String toString() {
341            return '[' + k + '\'' + op + '\'' + v + ']';
342        }
343    }
344
345    /**
346     * This condition requires a fixed key to match a given regexp
347     */
348    public static class KeyValueRegexpCondition extends KeyValueCondition {
349        protected static final Set<Op> SUPPORTED_OPS = EnumSet.of(Op.REGEX, Op.NREGEX);
350
351        final Pattern pattern;
352
353        /**
354         * Constructs a new {@code KeyValueRegexpCondition}.
355         * @param k key
356         * @param v value
357         * @param op operation
358         * @param considerValAsKey must be false
359         * @throws PatternSyntaxException if the value syntax is invalid
360         */
361        public KeyValueRegexpCondition(String k, String v, Op op, boolean considerValAsKey) {
362            super(k, v, op, considerValAsKey);
363            CheckParameterUtil.ensureThat(!considerValAsKey, "considerValAsKey is not supported");
364            CheckParameterUtil.ensureThat(SUPPORTED_OPS.contains(op), "Op must be REGEX or NREGEX");
365            this.pattern = Pattern.compile(v);
366        }
367
368        protected boolean matches(Environment env) {
369            final String value = env.osm.get(k);
370            return value != null && pattern.matcher(value).find();
371        }
372
373        @Override
374        public boolean applies(Environment env) {
375            if (Op.REGEX == op) {
376                return matches(env);
377            } else if (Op.NREGEX == op) {
378                return !matches(env);
379            } else {
380                throw new IllegalStateException();
381            }
382        }
383    }
384
385    /**
386     * A condition that checks that a key with the matching pattern has a value with the matching pattern.
387     */
388    public static class RegexpKeyValueRegexpCondition extends KeyValueRegexpCondition {
389
390        final Pattern keyPattern;
391
392        /**
393         * Create a condition in which the key and the value need to match a given regexp
394         * @param k The key regexp
395         * @param v The value regexp
396         * @param op The operation to use when comparing the key and the value.
397         */
398        public RegexpKeyValueRegexpCondition(String k, String v, Op op) {
399            super(k, v, op, false);
400            this.keyPattern = Pattern.compile(k);
401        }
402
403        @Override
404        public boolean requiresExactKeyMatch() {
405            return false;
406        }
407
408        @Override
409        protected boolean matches(Environment env) {
410            for (Map.Entry<String, String> kv: env.osm.getKeys().entrySet()) {
411                if (keyPattern.matcher(kv.getKey()).find() && pattern.matcher(kv.getValue()).find()) {
412                    return true;
413                }
414            }
415            return false;
416        }
417    }
418
419    /**
420     * Role condition.
421     */
422    public static class RoleCondition implements Condition {
423        final String role;
424        final Op op;
425
426        /**
427         * Constructs a new {@code RoleCondition}.
428         * @param role role
429         * @param op operation
430         */
431        public RoleCondition(String role, Op op) {
432            this.role = role;
433            this.op = op;
434        }
435
436        @Override
437        public boolean applies(Environment env) {
438            String testRole = env.getRole();
439            if (testRole == null) return false;
440            return op.eval(testRole, role);
441        }
442    }
443
444    /**
445     * Index condition.
446     */
447    public static class IndexCondition implements Condition {
448        final String index;
449        final Op op;
450
451        /**
452         * Constructs a new {@code IndexCondition}.
453         * @param index index
454         * @param op operation
455         */
456        public IndexCondition(String index, Op op) {
457            this.index = index;
458            this.op = op;
459        }
460
461        @Override
462        public boolean applies(Environment env) {
463            if (env.index == null) return false;
464            if (index.startsWith("-")) {
465                return env.count != null && op.eval(Integer.toString(env.index - env.count), index);
466            } else {
467                return op.eval(Integer.toString(env.index + 1), index);
468            }
469        }
470    }
471
472    /**
473     * This defines how {@link KeyCondition} matches a given key.
474     */
475    public enum KeyMatchType {
476        /**
477         * The key needs to be equal to the given label.
478         */
479        EQ,
480        /**
481         * The key needs to have a true value (yes, ...)
482         * @see OsmUtils#isTrue(String)
483         */
484        TRUE,
485        /**
486         * The key needs to have a false value (no, ...)
487         * @see OsmUtils#isFalse(String)
488         */
489        FALSE,
490        /**
491         * The key needs to match the given regular expression.
492         */
493        REGEX
494    }
495
496    /**
497     * <p>KeyCondition represent one of the following conditions in either the link or the
498     * primitive context:</p>
499     * <pre>
500     *     ["a label"]  PRIMITIVE:   the primitive has a tag "a label"
501     *                  LINK:        the parent is a relation and it has at least one member with the role
502     *                               "a label" referring to the child
503     *
504     *     [!"a label"]  PRIMITIVE:  the primitive doesn't have a tag "a label"
505     *                   LINK:       the parent is a relation but doesn't have a member with the role
506     *                               "a label" referring to the child
507     *
508     *     ["a label"?]  PRIMITIVE:  the primitive has a tag "a label" whose value evaluates to a true-value
509     *                   LINK:       not supported
510     *
511     *     ["a label"?!] PRIMITIVE:  the primitive has a tag "a label" whose value evaluates to a false-value
512     *                   LINK:       not supported
513     * </pre>
514     */
515    public static class KeyCondition implements Condition, ToTagConvertable {
516
517        /**
518         * The key name.
519         */
520        public final String label;
521        /**
522         * If we should negate the result of the match.
523         */
524        public final boolean negateResult;
525        /**
526         * Describes how to match the label against the key.
527         * @see KeyMatchType
528         */
529        public final KeyMatchType matchType;
530        /**
531         * A predicate used to match a the regexp against the key. Only used if the match type is regexp.
532         */
533        public final Predicate<String> containsPattern;
534
535        /**
536         * Creates a new KeyCondition
537         * @param label The key name (or regexp) to use.
538         * @param negateResult If we should negate the result.,
539         * @param matchType The match type.
540         */
541        public KeyCondition(String label, boolean negateResult, KeyMatchType matchType) {
542            this.label = label;
543            this.negateResult = negateResult;
544            this.matchType = matchType == null ? KeyMatchType.EQ : matchType;
545            this.containsPattern = KeyMatchType.REGEX == matchType
546                    ? Pattern.compile(label).asPredicate()
547                    : null;
548        }
549
550        @Override
551        public boolean applies(Environment e) {
552            switch(e.getContext()) {
553            case PRIMITIVE:
554                switch (matchType) {
555                case TRUE:
556                    return e.osm.isKeyTrue(label) ^ negateResult;
557                case FALSE:
558                    return e.osm.isKeyFalse(label) ^ negateResult;
559                case REGEX:
560                    return e.osm.keySet().stream().anyMatch(containsPattern) ^ negateResult;
561                default:
562                    return e.osm.hasKey(label) ^ negateResult;
563                }
564            case LINK:
565                Utils.ensure(false, "Illegal state: KeyCondition not supported in LINK context");
566                return false;
567            default: throw new AssertionError();
568            }
569        }
570
571        /**
572         * Get the matched key and the corresponding value.
573         * <p>
574         * WARNING: This ignores {@link #negateResult}.
575         * <p>
576         * WARNING: For regexp, the regular expression is returned instead of a key if the match failed.
577         * @param p The primitive to get the value from.
578         * @return The tag.
579         */
580        @Override
581        public Tag asTag(OsmPrimitive p) {
582            String key = label;
583            if (KeyMatchType.REGEX == matchType) {
584                key = p.keySet().stream().filter(containsPattern).findAny().orElse(key);
585            }
586            return new Tag(key, p.get(key));
587        }
588
589        @Override
590        public String toString() {
591            return '[' + (negateResult ? "!" : "") + label + ']';
592        }
593    }
594
595    /**
596     * Class condition.
597     */
598    public static class ClassCondition implements Condition {
599
600        /** Class identifier */
601        public final String id;
602        final boolean not;
603
604        /**
605         * Constructs a new {@code ClassCondition}.
606         * @param id id
607         * @param not negation or not
608         */
609        public ClassCondition(String id, boolean not) {
610            this.id = id;
611            this.not = not;
612        }
613
614        @Override
615        public boolean applies(Environment env) {
616            Cascade cascade = env.getCascade(env.layer);
617            return cascade != null && (not ^ cascade.containsKey(id));
618        }
619
620        @Override
621        public String toString() {
622            return (not ? "!" : "") + '.' + id;
623        }
624    }
625
626    /**
627     * Like <a href="http://www.w3.org/TR/css3-selectors/#pseudo-classes">CSS pseudo classes</a>, MapCSS pseudo classes
628     * are written in lower case with dashes between words.
629     */
630    public static final class PseudoClasses {
631
632        private PseudoClasses() {
633            // Hide default constructor for utilities classes
634        }
635
636        /**
637         * {@code closed} tests whether the way is closed or the relation is a closed multipolygon
638         * @param e MapCSS environment
639         * @return {@code true} if the way is closed or the relation is a closed multipolygon
640         */
641        static boolean closed(Environment e) { // NO_UCD (unused code)
642            if (e.osm instanceof IWay<?> && ((IWay<?>) e.osm).isClosed())
643                return true;
644            return e.osm instanceof IRelation<?> && ((IRelation<?>) e.osm).isMultipolygon();
645        }
646
647        /**
648         * {@code :modified} tests whether the object has been modified.
649         * @param e MapCSS environment
650         * @return {@code true} if the object has been modified
651         * @see IPrimitive#isModified()
652         */
653        static boolean modified(Environment e) { // NO_UCD (unused code)
654            return e.osm.isModified() || e.osm.isNewOrUndeleted();
655        }
656
657        /**
658         * {@code ;new} tests whether the object is new.
659         * @param e MapCSS environment
660         * @return {@code true} if the object is new
661         * @see IPrimitive#isNew()
662         */
663        static boolean _new(Environment e) { // NO_UCD (unused code)
664            return e.osm.isNew();
665        }
666
667        /**
668         * {@code :connection} tests whether the object is a connection node.
669         * @param e MapCSS environment
670         * @return {@code true} if the object is a connection node
671         * @see Node#isConnectionNode()
672         */
673        static boolean connection(Environment e) { // NO_UCD (unused code)
674            return e.osm instanceof INode && e.osm.getDataSet() != null && ((INode) e.osm).isConnectionNode();
675        }
676
677        /**
678         * {@code :tagged} tests whether the object is tagged.
679         * @param e MapCSS environment
680         * @return {@code true} if the object is tagged
681         * @see IPrimitive#isTagged()
682         */
683        static boolean tagged(Environment e) { // NO_UCD (unused code)
684            return e.osm.isTagged();
685        }
686
687        /**
688         * {@code :same-tags} tests whether the object has the same tags as its child/parent.
689         * @param e MapCSS environment
690         * @return {@code true} if the object has the same tags as its child/parent
691         * @see IPrimitive#hasSameInterestingTags(IPrimitive)
692         */
693        static boolean sameTags(Environment e) { // NO_UCD (unused code)
694            return e.osm.hasSameInterestingTags(Utils.firstNonNull(e.child, e.parent));
695        }
696
697        /**
698         * {@code :area-style} tests whether the object has an area style. This is useful for validators.
699         * @param e MapCSS environment
700         * @return {@code true} if the object has an area style
701         * @see ElemStyles#hasAreaElemStyle(IPrimitive, boolean)
702         */
703        static boolean areaStyle(Environment e) { // NO_UCD (unused code)
704            // only for validator
705            return ElemStyles.hasAreaElemStyle(e.osm, false);
706        }
707
708        /**
709         * {@code unconnected}: tests whether the object is a unconnected node.
710         * @param e MapCSS environment
711         * @return {@code true} if the object is a unconnected node
712         */
713        static boolean unconnected(Environment e) { // NO_UCD (unused code)
714            return e.osm instanceof Node && ((Node) e.osm).getParentWays().isEmpty();
715        }
716
717        /**
718         * {@code righthandtraffic} checks if there is right-hand traffic at the current location.
719         * @param e MapCSS environment
720         * @return {@code true} if there is right-hand traffic at the current location
721         * @see ExpressionFactory.Functions#is_right_hand_traffic(Environment)
722         */
723        static boolean righthandtraffic(Environment e) { // NO_UCD (unused code)
724            return ExpressionFactory.Functions.is_right_hand_traffic(e);
725        }
726
727        /**
728         * {@code clockwise} whether the way is closed and oriented clockwise,
729         * or non-closed and the 1st, 2nd and last node are in clockwise order.
730         * @param e MapCSS environment
731         * @return {@code true} if the way clockwise
732         * @see ExpressionFactory.Functions#is_clockwise(Environment)
733         */
734        static boolean clockwise(Environment e) { // NO_UCD (unused code)
735            return ExpressionFactory.Functions.is_clockwise(e);
736        }
737
738        /**
739         * {@code anticlockwise} whether the way is closed and oriented anticlockwise,
740         * or non-closed and the 1st, 2nd and last node are in anticlockwise order.
741         * @param e MapCSS environment
742         * @return {@code true} if the way clockwise
743         * @see ExpressionFactory.Functions#is_anticlockwise(Environment)
744         */
745        static boolean anticlockwise(Environment e) { // NO_UCD (unused code)
746            return ExpressionFactory.Functions.is_anticlockwise(e);
747        }
748
749        /**
750         * {@code unclosed-multipolygon} tests whether the object is an unclosed multipolygon.
751         * @param e MapCSS environment
752         * @return {@code true} if the object is an unclosed multipolygon
753         */
754        static boolean unclosed_multipolygon(Environment e) { // NO_UCD (unused code)
755            return e.osm instanceof Relation && ((Relation) e.osm).isMultipolygon() &&
756                    !e.osm.isIncomplete() && !((Relation) e.osm).hasIncompleteMembers() &&
757                    !MultipolygonCache.getInstance().get((Relation) e.osm).getOpenEnds().isEmpty();
758        }
759
760        private static final Predicate<OsmPrimitive> IN_DOWNLOADED_AREA = new InDataSourceArea(false);
761
762        /**
763         * {@code in-downloaded-area} tests whether the object is within source area ("downloaded area").
764         * @param e MapCSS environment
765         * @return {@code true} if the object is within source area ("downloaded area")
766         * @see InDataSourceArea
767         */
768        static boolean inDownloadedArea(Environment e) { // NO_UCD (unused code)
769            return e.osm instanceof OsmPrimitive && IN_DOWNLOADED_AREA.test((OsmPrimitive) e.osm);
770        }
771
772        static boolean completely_downloaded(Environment e) { // NO_UCD (unused code)
773            if (e.osm instanceof IRelation<?>) {
774                return !((IRelation<?>) e.osm).hasIncompleteMembers();
775            } else {
776                return true;
777            }
778        }
779
780        static boolean closed2(Environment e) { // NO_UCD (unused code)
781            if (e.osm instanceof IWay<?> && ((IWay<?>) e.osm).isClosed())
782                return true;
783            if (e.osm instanceof Relation && ((Relation) e.osm).isMultipolygon()) {
784                Multipolygon multipolygon = MultipolygonCache.getInstance().get((Relation) e.osm);
785                return multipolygon != null && multipolygon.getOpenEnds().isEmpty();
786            }
787            return false;
788        }
789
790        static boolean selected(Environment e) { // NO_UCD (unused code)
791            if (e.mc != null) {
792                e.mc.getCascade(e.layer).setDefaultSelectedHandling(false);
793            }
794            return e.osm.isSelected();
795        }
796    }
797
798    /**
799     * Pseudo class condition.
800     */
801    public static class PseudoClassCondition implements Condition {
802
803        final Method method;
804        final boolean not;
805
806        protected PseudoClassCondition(Method method, boolean not) {
807            this.method = method;
808            this.not = not;
809        }
810
811        /**
812         * Create a new pseudo class condition
813         * @param id The id of the pseudo class
814         * @param not <code>true</code> to invert the condition
815         * @param context The context the class is found in.
816         * @return The new condition
817         */
818        public static PseudoClassCondition createPseudoClassCondition(String id, boolean not, Context context) {
819            CheckParameterUtil.ensureThat(!"sameTags".equals(id) || Context.LINK == context, "sameTags only supported in LINK context");
820            if ("open_end".equals(id)) {
821                return new OpenEndPseudoClassCondition(not);
822            }
823            final Method method = getMethod(id);
824            if (method != null) {
825                return new PseudoClassCondition(method, not);
826            }
827            throw new MapCSSException("Invalid pseudo class specified: " + id);
828        }
829
830        protected static Method getMethod(String id) {
831            String cleanId = id.replaceAll("-|_", "");
832            for (Method method : PseudoClasses.class.getDeclaredMethods()) {
833                // for backwards compatibility, consider :sameTags == :same-tags == :same_tags (#11150)
834                final String methodName = method.getName().replaceAll("-|_", "");
835                if (methodName.equalsIgnoreCase(cleanId)) {
836                    return method;
837                }
838            }
839            return null;
840        }
841
842        @Override
843        public boolean applies(Environment e) {
844            try {
845                return not ^ (Boolean) method.invoke(null, e);
846            } catch (ReflectiveOperationException ex) {
847                throw new JosmRuntimeException(ex);
848            }
849        }
850
851        @Override
852        public String toString() {
853            return (not ? "!" : "") + ':' + method.getName();
854        }
855    }
856
857    /**
858     * Open end pseudo class condition.
859     */
860    public static class OpenEndPseudoClassCondition extends PseudoClassCondition {
861        /**
862         * Constructs a new {@code OpenEndPseudoClassCondition}.
863         * @param not negation or not
864         */
865        public OpenEndPseudoClassCondition(boolean not) {
866            super(null, not);
867        }
868
869        @Override
870        public boolean applies(Environment e) {
871            return true;
872        }
873    }
874
875    /**
876     * A condition that is fulfilled whenever the expression is evaluated to be true.
877     */
878    public static class ExpressionCondition implements Condition {
879
880        final Expression e;
881
882        /**
883         * Constructs a new {@code ExpressionFactory}
884         * @param e expression
885         */
886        public ExpressionCondition(Expression e) {
887            this.e = e;
888        }
889
890        /**
891         * Returns the expression.
892         * @return the expression
893         * @since 14484
894         */
895        public final Expression getExpression() {
896            return e;
897        }
898
899        @Override
900        public boolean applies(Environment env) {
901            Boolean b = Cascade.convertTo(e.evaluate(env), Boolean.class);
902            return b != null && b;
903        }
904
905        @Override
906        public String toString() {
907            return '[' + e.toString() + ']';
908        }
909    }
910}