001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.mappaint.mapcss;
003
004import java.text.MessageFormat;
005import java.util.Arrays;
006import java.util.EnumSet;
007import java.util.Objects;
008import java.util.Set;
009import java.util.regex.Pattern;
010
011import org.openstreetmap.josm.data.osm.Node;
012import org.openstreetmap.josm.data.osm.OsmPrimitive;
013import org.openstreetmap.josm.data.osm.Relation;
014import org.openstreetmap.josm.data.osm.Tag;
015import org.openstreetmap.josm.data.osm.Way;
016import org.openstreetmap.josm.gui.mappaint.Cascade;
017import org.openstreetmap.josm.gui.mappaint.ElemStyles;
018import org.openstreetmap.josm.gui.mappaint.Environment;
019import org.openstreetmap.josm.tools.CheckParameterUtil;
020import org.openstreetmap.josm.tools.Predicate;
021import org.openstreetmap.josm.tools.Predicates;
022import org.openstreetmap.josm.tools.Utils;
023
024public abstract class Condition {
025
026    public abstract boolean applies(Environment e);
027
028    public static Condition createKeyValueCondition(String k, String v, Op op, Context context, boolean considerValAsKey) {
029        switch (context) {
030        case PRIMITIVE:
031            if (KeyValueRegexpCondition.SUPPORTED_OPS.contains(op) && !considerValAsKey)
032                return new KeyValueRegexpCondition(k, v, op, false);
033            if (!considerValAsKey && op.equals(Op.EQ))
034                return new SimpleKeyValueCondition(k, v);
035            return new KeyValueCondition(k, v, op, considerValAsKey);
036        case LINK:
037            if (considerValAsKey)
038                throw new MapCSSException("''considerValAsKey'' not supported in LINK context");
039            if ("role".equalsIgnoreCase(k))
040                return new RoleCondition(v, op);
041            else if ("index".equalsIgnoreCase(k))
042                return new IndexCondition(v, op);
043            else
044                throw new MapCSSException(
045                        MessageFormat.format("Expected key ''role'' or ''index'' in link context. Got ''{0}''.", k));
046
047        default: throw new AssertionError();
048        }
049    }
050
051    public static Condition createKeyCondition(String k, boolean not, KeyMatchType matchType, Context context) {
052        switch (context) {
053        case PRIMITIVE:
054            return new KeyCondition(k, not, matchType);
055        case LINK:
056            if (matchType != null)
057                throw new MapCSSException("Question mark operator ''?'' and regexp match not supported in LINK context");
058            if (not)
059                return new RoleCondition(k, Op.NEQ);
060            else
061                return new RoleCondition(k, Op.EQ);
062
063        default: throw new AssertionError();
064        }
065    }
066
067    public static PseudoClassCondition createPseudoClassCondition(String id, boolean not, Context context) {
068        return new PseudoClassCondition(id, not, context);
069    }
070
071    public static ClassCondition createClassCondition(String id, boolean not, Context context) {
072        return new ClassCondition(id, not);
073    }
074
075    public static ExpressionCondition createExpressionCondition(Expression e, Context context) {
076        return new ExpressionCondition(e);
077    }
078
079    public static enum Op {
080        EQ, NEQ, GREATER_OR_EQUAL, GREATER, LESS_OR_EQUAL, LESS,
081        REGEX, NREGEX, ONE_OF, BEGINS_WITH, ENDS_WITH, CONTAINS;
082
083        private static final Set<Op> NEGATED_OPS = EnumSet.of(NEQ, NREGEX);
084
085        public boolean eval(String testString, String prototypeString) {
086            if (testString == null && !NEGATED_OPS.contains(this))
087                return false;
088            switch (this) {
089            case EQ:
090                return Objects.equals(testString, prototypeString);
091            case NEQ:
092                return !Objects.equals(testString, prototypeString);
093            case REGEX:
094            case NREGEX:
095                final boolean contains = Pattern.compile(prototypeString).matcher(testString).find();
096                return REGEX.equals(this) ? contains : !contains;
097            case ONE_OF:
098                return Arrays.asList(testString.split("\\s*;\\s*")).contains(prototypeString);
099            case BEGINS_WITH:
100                return testString.startsWith(prototypeString);
101            case ENDS_WITH:
102                return testString.endsWith(prototypeString);
103            case CONTAINS:
104                return testString.contains(prototypeString);
105            }
106
107            float test_float;
108            try {
109                test_float = Float.parseFloat(testString);
110            } catch (NumberFormatException e) {
111                return false;
112            }
113            float prototype_float = Float.parseFloat(prototypeString);
114
115            switch (this) {
116            case GREATER_OR_EQUAL:
117                return test_float >= prototype_float;
118            case GREATER:
119                return test_float > prototype_float;
120            case LESS_OR_EQUAL:
121                return test_float <= prototype_float;
122            case LESS:
123                return test_float < prototype_float;
124            default:
125                throw new AssertionError();
126            }
127        }
128    }
129
130    /**
131     * Context, where the condition applies.
132     */
133    public static enum Context {
134        /**
135         * normal primitive selector, e.g. way[highway=residential]
136         */
137        PRIMITIVE,
138
139        /**
140         * link between primitives, e.g. relation &gt;[role=outer] way
141         */
142        LINK
143    }
144
145    public static final EnumSet<Op> COMPARISON_OPERATERS =
146        EnumSet.of(Op.GREATER_OR_EQUAL, Op.GREATER, Op.LESS_OR_EQUAL, Op.LESS);
147
148    /**
149     * Most common case of a KeyValueCondition.
150     *
151     * Extra class for performance reasons.
152     */
153    public static class SimpleKeyValueCondition extends Condition {
154        public final String k;
155        public final String v;
156
157        public SimpleKeyValueCondition(String k, String v) {
158            this.k = k;
159            this.v = v;
160        }
161
162        @Override
163        public boolean applies(Environment e) {
164            return v.equals(e.osm.get(k));
165        }
166
167        public Tag asTag() {
168            return new Tag(k, v);
169        }
170
171        @Override
172        public String toString() {
173            return '[' + k + '=' + v + ']';
174        }
175
176    }
177
178    /**
179     * <p>Represents a key/value condition which is either applied to a primitive.</p>
180     *
181     */
182    public static class KeyValueCondition extends Condition {
183
184        public final String k;
185        public final String v;
186        public final Op op;
187        public boolean considerValAsKey;
188
189        /**
190         * <p>Creates a key/value-condition.</p>
191         *
192         * @param k the key
193         * @param v the value
194         * @param op the operation
195         * @param considerValAsKey whether to consider {@code v} as another key and compare the values of key {@code k} and key {@code v}.
196         */
197        public KeyValueCondition(String k, String v, Op op, boolean considerValAsKey) {
198            this.k = k;
199            this.v = v;
200            this.op = op;
201            this.considerValAsKey = considerValAsKey;
202        }
203
204        @Override
205        public boolean applies(Environment env) {
206            return op.eval(env.osm.get(k), considerValAsKey ? env.osm.get(v) : v);
207        }
208
209        public Tag asTag() {
210            return new Tag(k, v);
211        }
212
213        @Override
214        public String toString() {
215            return "[" + k + "'" + op + "'" + v + "]";
216        }
217    }
218
219    public static class KeyValueRegexpCondition extends KeyValueCondition {
220
221        public final Pattern pattern;
222        public static final EnumSet<Op> SUPPORTED_OPS = EnumSet.of(Op.REGEX, Op.NREGEX);
223
224        public KeyValueRegexpCondition(String k, String v, Op op, boolean considerValAsKey) {
225            super(k, v, op, considerValAsKey);
226            CheckParameterUtil.ensureThat(!considerValAsKey, "considerValAsKey is not supported");
227            CheckParameterUtil.ensureThat(SUPPORTED_OPS.contains(op), "Op must be REGEX or NREGEX");
228            this.pattern = Pattern.compile(v);
229        }
230
231        @Override
232        public boolean applies(Environment env) {
233            final String value = env.osm.get(k);
234            if (Op.REGEX.equals(op)) {
235                return value != null && pattern.matcher(value).find();
236            } else if (Op.NREGEX.equals(op)) {
237                return value == null || !pattern.matcher(value).find();
238            } else {
239                throw new IllegalStateException();
240            }
241        }
242    }
243
244    public static class RoleCondition extends Condition {
245        public final String role;
246        public final Op op;
247
248        public RoleCondition(String role, Op op) {
249            this.role = role;
250            this.op = op;
251        }
252
253        @Override
254        public boolean applies(Environment env) {
255            String testRole = env.getRole();
256            if (testRole == null) return false;
257            return op.eval(testRole, role);
258        }
259    }
260
261    public static class IndexCondition extends Condition {
262        public final String index;
263        public final Op op;
264
265        public IndexCondition(String index, Op op) {
266            this.index = index;
267            this.op = op;
268        }
269
270        @Override
271        public boolean applies(Environment env) {
272            if (env.index == null) return false;
273            return op.eval(Integer.toString(env.index + 1), index);
274        }
275    }
276
277    public static enum KeyMatchType {
278        EQ, TRUE, FALSE, REGEX
279    }
280
281    /**
282     * <p>KeyCondition represent one of the following conditions in either the link or the
283     * primitive context:</p>
284     * <pre>
285     *     ["a label"]  PRIMITIVE:   the primitive has a tag "a label"
286     *                  LINK:        the parent is a relation and it has at least one member with the role
287     *                               "a label" referring to the child
288     *
289     *     [!"a label"]  PRIMITIVE:  the primitive doesn't have a tag "a label"
290     *                   LINK:       the parent is a relation but doesn't have a member with the role
291     *                               "a label" referring to the child
292     *
293     *     ["a label"?]  PRIMITIVE:  the primitive has a tag "a label" whose value evaluates to a true-value
294     *                   LINK:       not supported
295     *
296     *     ["a label"?!] PRIMITIVE:  the primitive has a tag "a label" whose value evaluates to a false-value
297     *                   LINK:       not supported
298     * </pre>
299     */
300    public static class KeyCondition extends Condition {
301
302        public final String label;
303        public final boolean negateResult;
304        public final KeyMatchType matchType;
305        public Predicate<String> containsPattern;
306
307        public KeyCondition(String label, boolean negateResult, KeyMatchType matchType){
308            this.label = label;
309            this.negateResult = negateResult;
310            this.matchType = matchType;
311            this.containsPattern = KeyMatchType.REGEX.equals(matchType)
312                    ? Predicates.stringContainsPattern(Pattern.compile(label))
313                    : null;
314        }
315
316        @Override
317        public boolean applies(Environment e) {
318            switch(e.getContext()) {
319            case PRIMITIVE:
320                if (KeyMatchType.TRUE.equals(matchType))
321                    return e.osm.isKeyTrue(label) ^ negateResult;
322                else if (KeyMatchType.FALSE.equals(matchType))
323                    return e.osm.isKeyFalse(label) ^ negateResult;
324                else if (KeyMatchType.REGEX.equals(matchType)) {
325                    return Utils.exists(e.osm.keySet(), containsPattern) ^ negateResult;
326                } else {
327                    return e.osm.hasKey(label) ^ negateResult;
328                }
329            case LINK:
330                Utils.ensure(false, "Illegal state: KeyCondition not supported in LINK context");
331                return false;
332            default: throw new AssertionError();
333            }
334        }
335
336        public Tag asTag() {
337            return new Tag(label);
338        }
339
340        @Override
341        public String toString() {
342            return "[" + (negateResult ? "!" : "") + label + "]";
343        }
344    }
345
346    public static class ClassCondition extends Condition {
347
348        public final String id;
349        public final boolean not;
350
351        public ClassCondition(String id, boolean not) {
352            this.id = id;
353            this.not = not;
354        }
355
356        @Override
357        public boolean applies(Environment env) {
358            return env != null && env.getCascade(env.layer) != null && not ^ env.getCascade(env.layer).containsKey(id);
359        }
360
361        @Override
362        public String toString() {
363            return (not ? "!" : "") + "." + id;
364        }
365    }
366
367    public static class PseudoClassCondition extends Condition {
368
369        public final String id;
370        public final boolean not;
371
372        public PseudoClassCondition(String id, boolean not, Context context) {
373            this.id = id;
374            this.not = not;
375            CheckParameterUtil.ensureThat(!"sameTags".equals(id) || Context.LINK.equals(context), "sameTags only supported in LINK context");
376        }
377
378        @Override
379        public boolean applies(Environment e) {
380            return not ^ appliesImpl(e);
381        }
382
383        public boolean appliesImpl(Environment e) {
384            switch(id) {
385            case "closed":
386                if (e.osm instanceof Way && ((Way) e.osm).isClosed())
387                    return true;
388                if (e.osm instanceof Relation && ((Relation) e.osm).isMultipolygon())
389                    return true;
390                break;
391            case "modified":
392                return e.osm.isModified() || e.osm.isNewOrUndeleted();
393            case "new":
394                return e.osm.isNew();
395            case "connection":
396                return e.osm instanceof Node && ((Node) e.osm).isConnectionNode();
397            case "tagged":
398                return e.osm.isTagged();
399            case "sameTags":
400                return e.osm.hasSameInterestingTags(Utils.firstNonNull(e.child, e.parent));
401            case "areaStyle":
402                // only for validator
403                return ElemStyles.hasAreaElemStyle(e.osm, false);
404            case "unconnected":
405                return e.osm instanceof Node && OsmPrimitive.getFilteredList(e.osm.getReferrers(), Way.class).isEmpty();
406            case "righthandtraffic":
407                return ExpressionFactory.Functions.is_right_hand_traffic(e);
408            }
409            return false;
410        }
411
412        @Override
413        public String toString() {
414            return ":" + (not ? "!" : "") + id;
415        }
416    }
417
418    public static class ExpressionCondition extends Condition {
419
420        private final Expression e;
421
422        public ExpressionCondition(Expression e) {
423            this.e = e;
424        }
425
426        @Override
427        public boolean applies(Environment env) {
428            Boolean b = Cascade.convertTo(e.evaluate(env), Boolean.class);
429            return b != null && b;
430        }
431
432        @Override
433        public String toString() {
434            return "[" + e + "]";
435        }
436    }
437}