001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.mappaint.mapcss;
003
004import static org.openstreetmap.josm.tools.Utils.equal;
005
006import java.text.MessageFormat;
007import java.util.EnumSet;
008import java.util.regex.Matcher;
009import java.util.regex.Pattern;
010
011import org.openstreetmap.josm.data.osm.Node;
012import org.openstreetmap.josm.data.osm.OsmUtils;
013import org.openstreetmap.josm.data.osm.Relation;
014import org.openstreetmap.josm.data.osm.Way;
015import org.openstreetmap.josm.gui.mappaint.Cascade;
016import org.openstreetmap.josm.gui.mappaint.Environment;
017import org.openstreetmap.josm.tools.Utils;
018
019abstract public class Condition {
020
021    abstract public boolean applies(Environment e);
022
023    public static Condition create(String k, String v, Op op, Context context) {
024        switch (context) {
025        case PRIMITIVE:
026            return new KeyValueCondition(k, v, op);
027        case LINK:
028            if ("role".equalsIgnoreCase(k))
029                return new RoleCondition(v, op);
030            else if ("index".equalsIgnoreCase(k))
031                return new IndexCondition(v, op);
032            else
033                throw new MapCSSException(
034                        MessageFormat.format("Expected key ''role'' or ''index'' in link context. Got ''{0}''.", k));
035
036        default: throw new AssertionError();
037        }
038    }
039
040    public static Condition create(String k, boolean not, boolean yes, Context context) {
041        switch (context) {
042        case PRIMITIVE:
043            return new KeyCondition(k, not, yes);
044        case LINK:
045            if (yes)
046                throw new MapCSSException("Question mark operator ''?'' not supported in LINK context");
047            if (not)
048                return new RoleCondition(k, Op.NEQ);
049            else
050                return new RoleCondition(k, Op.EQ);
051
052        default: throw new AssertionError();
053        }
054    }
055
056    public static Condition create(String id, boolean not, Context context) {
057        return new PseudoClassCondition(id, not);
058    }
059
060    public static Condition create(Expression e, Context context) {
061        return new ExpressionCondition(e);
062    }
063
064    public static enum Op {
065        EQ, NEQ, GREATER_OR_EQUAL, GREATER, LESS_OR_EQUAL, LESS,
066        REGEX, NREGEX, ONE_OF, BEGINS_WITH, ENDS_WITH, CONTAINS;
067
068        public boolean eval(String testString, String prototypeString) {
069            if (testString == null && this != NEQ)
070                return false;
071            switch (this) {
072            case EQ:
073                return equal(testString, prototypeString);
074            case NEQ:
075                return !equal(testString, prototypeString);
076            case REGEX:
077            case NREGEX:
078                Pattern p = Pattern.compile(prototypeString);
079                Matcher m = p.matcher(testString);
080                return REGEX.equals(this) ? m.find() : !m.find();
081            case ONE_OF:
082                String[] parts = testString.split(";");
083                for (String part : parts) {
084                    if (equal(prototypeString, part.trim()))
085                        return true;
086                }
087                return false;
088            case BEGINS_WITH:
089                return testString.startsWith(prototypeString);
090            case ENDS_WITH:
091                return testString.endsWith(prototypeString);
092            case CONTAINS:
093                return testString.contains(prototypeString);
094            }
095
096            float test_float;
097            try {
098                test_float = Float.parseFloat(testString);
099            } catch (NumberFormatException e) {
100                return false;
101            }
102            float prototype_float = Float.parseFloat(prototypeString);
103
104            switch (this) {
105            case GREATER_OR_EQUAL:
106                return test_float >= prototype_float;
107            case GREATER:
108                return test_float > prototype_float;
109            case LESS_OR_EQUAL:
110                return test_float <= prototype_float;
111            case LESS:
112                return test_float < prototype_float;
113            default:
114                throw new AssertionError();
115            }
116        }
117    }
118
119    /**
120     * context, where the condition applies
121     */
122    public static enum Context {
123        /**
124         * normal primitive selector, e.g. way[highway=residential]
125         */
126        PRIMITIVE,
127
128        /**
129         * link between primitives, e.g. relation >[role=outer] way
130         */
131        LINK
132    }
133
134    public final static EnumSet<Op> COMPARISON_OPERATERS =
135        EnumSet.of(Op.GREATER_OR_EQUAL, Op.GREATER, Op.LESS_OR_EQUAL, Op.LESS);
136
137    /**
138     * <p>Represents a key/value condition which is either applied to a primitive.</p>
139     *
140     */
141    public static class KeyValueCondition extends Condition {
142
143        public String k;
144        public String v;
145        public Op op;
146
147        /**
148         * <p>Creates a key/value-condition.</p>
149         *
150         * @param k the key
151         * @param v the value
152         * @param op the operation
153         */
154        public KeyValueCondition(String k, String v, Op op) {
155            this.k = k;
156            this.v = v;
157            this.op = op;
158        }
159
160        @Override
161        public boolean applies(Environment env) {
162            return op.eval(env.osm.get(k), v);
163        }
164
165        @Override
166        public String toString() {
167            return "[" + k + "'" + op + "'" + v + "]";
168        }
169    }
170
171    public static class RoleCondition extends Condition {
172        public String role;
173        public Op op;
174
175        public RoleCondition(String role, Op op) {
176            this.role = role;
177            this.op = op;
178        }
179
180        @Override
181        public boolean applies(Environment env) {
182            String testRole = env.getRole();
183            if (testRole == null) return false;
184            return op.eval(testRole, role);
185        }
186    }
187
188    public static class IndexCondition extends Condition {
189        public String index;
190        public Op op;
191
192        public IndexCondition(String index, Op op) {
193            this.index = index;
194            this.op = op;
195        }
196
197        @Override
198        public boolean applies(Environment env) {
199            if (env.index == null) return false;
200            return op.eval(Integer.toString(env.index + 1), index);
201        }
202    }
203
204    /**
205     * <p>KeyCondition represent one of the following conditions in either the link or the
206     * primitive context:</p>
207     * <pre>
208     *     ["a label"]  PRIMITIVE:   the primitive has a tag "a label"
209     *                  LINK:        the parent is a relation and it has at least one member with the role
210     *                               "a label" referring to the child
211     *
212     *     [!"a label"]  PRIMITIVE:  the primitive doesn't have a tag "a label"
213     *                   LINK:       the parent is a relation but doesn't have a member with the role
214     *                               "a label" referring to the child
215     *
216     *     ["a label"?]  PRIMITIVE:  the primitive has a tag "a label" whose value evaluates to a true-value
217     *                   LINK:       not supported
218     * </pre>
219     */
220    public static class KeyCondition extends Condition {
221
222        private String label;
223        private boolean exclamationMarkPresent;
224        private boolean questionMarkPresent;
225
226        /**
227         *
228         * @param label
229         * @param exclamationMarkPresent
230         * @param questionMarkPresent
231         */
232        public KeyCondition(String label, boolean exclamationMarkPresent, boolean questionMarkPresent){
233            this.label = label;
234            this.exclamationMarkPresent = exclamationMarkPresent;
235            this.questionMarkPresent = questionMarkPresent;
236        }
237
238        @Override
239        public boolean applies(Environment e) {
240            switch(e.getContext()) {
241            case PRIMITIVE:
242                if (questionMarkPresent)
243                    return OsmUtils.isTrue(e.osm.get(label)) ^ exclamationMarkPresent;
244                else
245                    return e.osm.hasKey(label) ^ exclamationMarkPresent;
246            case LINK:
247                Utils.ensure(false, "Illegal state: KeyCondition not supported in LINK context");
248                return false;
249            default: throw new AssertionError();
250            }
251        }
252
253        @Override
254        public String toString() {
255            return "[" + (exclamationMarkPresent ? "!" : "") + label + "]";
256        }
257    }
258
259    public static class PseudoClassCondition extends Condition {
260
261        String id;
262        boolean not;
263
264        public PseudoClassCondition(String id, boolean not) {
265            this.id = id;
266            this.not = not;
267        }
268
269        @Override
270        public boolean applies(Environment e) {
271            return not ^ appliesImpl(e);
272        }
273
274        public boolean appliesImpl(Environment e) {
275            if (equal(id, "closed")) {
276                if (e.osm instanceof Way && ((Way) e.osm).isClosed())
277                    return true;
278                if (e.osm instanceof Relation && ((Relation) e.osm).isMultipolygon())
279                    return true;
280                return false;
281            } else if (equal(id, "modified"))
282                return e.osm.isModified() || e.osm.isNewOrUndeleted();
283            else if (equal(id, "new"))
284                return e.osm.isNew();
285            else if (equal(id, "connection") && (e.osm instanceof Node))
286                return ((Node) e.osm).isConnectionNode();
287            else if (equal(id, "tagged"))
288                return e.osm.isTagged();
289            return true;
290        }
291
292        @Override
293        public String toString() {
294            return ":" + (not ? "!" : "") + id;
295        }
296    }
297
298    public static class ExpressionCondition extends Condition {
299
300        private Expression e;
301
302        public ExpressionCondition(Expression e) {
303            this.e = e;
304        }
305
306        @Override
307        public boolean applies(Environment env) {
308            Boolean b = Cascade.convertTo(e.evaluate(env), Boolean.class);
309            return b != null && b;
310        }
311
312        @Override
313        public String toString() {
314            return "[" + e + "]";
315        }
316    }
317}