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 }