001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.mappaint.mapcss;
003
004import java.text.MessageFormat;
005import java.util.EnumSet;
006import java.util.HashMap;
007import java.util.Locale;
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.Tagged;
027import org.openstreetmap.josm.data.osm.search.SearchCompiler.InDataSourceArea;
028import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
029import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
030import org.openstreetmap.josm.gui.mappaint.Cascade;
031import org.openstreetmap.josm.gui.mappaint.ElemStyles;
032import org.openstreetmap.josm.gui.mappaint.Environment;
033import org.openstreetmap.josm.gui.mappaint.mapcss.Condition.Context;
034import org.openstreetmap.josm.gui.mappaint.mapcss.Condition.TagCondition;
035import org.openstreetmap.josm.tools.CheckParameterUtil;
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            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 mapt 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<?> && ((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 a unconnected node.
787         * @param e MapCSS environment
788         * @return {@code true} if the object is a 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 && ((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        static boolean completely_downloaded(Environment e) {
850            if (e.osm instanceof IRelation<?>) {
851                return !((IRelation<?>) e.osm).hasIncompleteMembers();
852            } else if (e.osm instanceof IWay<?>) {
853                return !((IWay<?>) e.osm).hasIncompleteNodes();
854            } else if (e.osm instanceof INode) {
855                return ((INode) e.osm).isLatLonKnown();
856            } else {
857                return true;
858            }
859        }
860
861        static boolean closed2(Environment e) {
862            if (e.osm instanceof IWay<?> && ((IWay<?>) e.osm).isClosed())
863                return true;
864            if (e.osm instanceof Relation && ((Relation) e.osm).isMultipolygon()) {
865                Multipolygon multipolygon = MultipolygonCache.getInstance().get((Relation) e.osm);
866                return multipolygon != null && multipolygon.getOpenEnds().isEmpty();
867            }
868            return false;
869        }
870
871        static boolean selected(Environment e) {
872            if (e.mc != null) {
873                e.getCascade().setDefaultSelectedHandling(false);
874            }
875            return e.osm.isSelected();
876        }
877
878        /**
879         * Check if the object is highlighted (i.e., is hovered over)
880         * @param e The MapCSS environment
881         * @return {@code true} if the object is highlighted
882         * @see IPrimitive#isHighlighted
883         * @since 17862
884         */
885        static boolean highlighted(Environment e) {
886            return e.osm.isHighlighted();
887        }
888    }
889
890    /**
891     * Pseudo class condition.
892     */
893    public static class PseudoClassCondition implements Condition {
894
895        static final Map<String, PseudoClassCondition> CONDITION_MAP = new HashMap<>();
896
897        static {
898            PseudoClassCondition.register("anticlockwise", PseudoClasses::anticlockwise);
899            PseudoClassCondition.register("areaStyle", PseudoClasses::areaStyle);
900            PseudoClassCondition.register("clockwise", PseudoClasses::clockwise);
901            PseudoClassCondition.register("closed", PseudoClasses::closed);
902            PseudoClassCondition.register("closed2", PseudoClasses::closed2);
903            PseudoClassCondition.register("completely_downloaded", PseudoClasses::completely_downloaded);
904            PseudoClassCondition.register("connection", PseudoClasses::connection);
905            PseudoClassCondition.register("highlighted", PseudoClasses::highlighted);
906            PseudoClassCondition.register("inDownloadedArea", PseudoClasses::inDownloadedArea);
907            PseudoClassCondition.register("modified", PseudoClasses::modified);
908            PseudoClassCondition.register("new", PseudoClasses::_new);
909            PseudoClassCondition.register("righthandtraffic", PseudoClasses::righthandtraffic);
910            PseudoClassCondition.register("sameTags", PseudoClasses::sameTags);
911            PseudoClassCondition.register("selected", PseudoClasses::selected);
912            PseudoClassCondition.register("tagged", PseudoClasses::tagged);
913            PseudoClassCondition.register("unclosed_multipolygon", PseudoClasses::unclosed_multipolygon);
914            PseudoClassCondition.register("unconnected", PseudoClasses::unconnected);
915        }
916
917        private static void register(String name, Predicate<Environment> predicate) {
918            CONDITION_MAP.put(clean(name), new PseudoClassCondition(":" + name, predicate));
919            CONDITION_MAP.put("!" + clean(name), new PseudoClassCondition("!:" + name, predicate.negate()));
920        }
921
922        private final String name;
923        private final Predicate<Environment> predicate;
924
925        protected PseudoClassCondition(String name, Predicate<Environment> predicate) {
926            this.name = name;
927            this.predicate = predicate;
928        }
929
930        /**
931         * Create a new pseudo class condition
932         * @param id The id of the pseudo class
933         * @param not <code>true</code> to invert the condition
934         * @param context The context the class is found in.
935         * @return The new condition
936         */
937        public static PseudoClassCondition createPseudoClassCondition(String id, boolean not, Context context) {
938            CheckParameterUtil.ensureThat(!"sameTags".equals(id) || Context.LINK == context, "sameTags only supported in LINK context");
939            if ("open_end".equals(id)) {
940                return new OpenEndPseudoClassCondition(not);
941            }
942            String cleanId = not ? clean("!" + id) : clean(id);
943            PseudoClassCondition condition = CONDITION_MAP.get(cleanId);
944            if (condition != null) {
945                return condition;
946            }
947            throw new MapCSSException("Invalid pseudo class specified: " + id);
948        }
949
950        private static String clean(String id) {
951            // for backwards compatibility, consider :sameTags == :same-tags == :same_tags (#11150)
952            return id.toLowerCase(Locale.ROOT).replaceAll("[-_]", "");
953        }
954
955        @Override
956        public boolean applies(Environment e) {
957            return predicate.test(e);
958        }
959
960        @Override
961        public String toString() {
962            return name;
963        }
964    }
965
966    /**
967     * Open end pseudo class condition.
968     */
969    public static class OpenEndPseudoClassCondition extends PseudoClassCondition {
970        final boolean not;
971        /**
972         * Constructs a new {@code OpenEndPseudoClassCondition}.
973         * @param not negation or not
974         */
975        public OpenEndPseudoClassCondition(boolean not) {
976            super("open_end", null);
977            this.not = not;
978        }
979
980        @Override
981        public boolean applies(Environment e) {
982            return !not;
983        }
984    }
985
986    /**
987     * A condition that is fulfilled whenever the expression is evaluated to be true.
988     */
989    public static class ExpressionCondition implements Condition {
990
991        final Expression e;
992
993        /**
994         * Constructs a new {@code ExpressionFactory}
995         * @param e expression
996         */
997        public ExpressionCondition(Expression e) {
998            this.e = e;
999        }
1000
1001        /**
1002         * Returns the expression.
1003         * @return the expression
1004         * @since 14484
1005         */
1006        public final Expression getExpression() {
1007            return e;
1008        }
1009
1010        @Override
1011        public boolean applies(Environment env) {
1012            Boolean b = Cascade.convertTo(e.evaluate(env), Boolean.class);
1013            return b != null && b;
1014        }
1015
1016        @Override
1017        public String toString() {
1018            return '[' + e.toString() + ']';
1019        }
1020    }
1021}