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