source: josm/trunk/src/org/openstreetmap/josm/gui/mappaint/mapcss/Condition.java@ 8266

Last change on this file since 8266 was 8266, checked in by simon04, 9 years ago

fix #9782 fix #10859 - MapCSS validator: evaluate real key and value for KeyConditions

  • Property svn:eol-style set to native
File size: 16.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.mappaint.mapcss;
3
4import java.text.MessageFormat;
5import java.util.Arrays;
6import java.util.Collection;
7import java.util.EnumSet;
8import java.util.Objects;
9import java.util.Set;
10import java.util.regex.Pattern;
11
12import org.openstreetmap.josm.Main;
13import org.openstreetmap.josm.data.osm.Node;
14import org.openstreetmap.josm.data.osm.OsmPrimitive;
15import org.openstreetmap.josm.data.osm.Relation;
16import org.openstreetmap.josm.data.osm.Tag;
17import org.openstreetmap.josm.data.osm.Way;
18import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
19import org.openstreetmap.josm.gui.mappaint.Cascade;
20import org.openstreetmap.josm.gui.mappaint.ElemStyles;
21import org.openstreetmap.josm.gui.mappaint.Environment;
22import org.openstreetmap.josm.tools.CheckParameterUtil;
23import org.openstreetmap.josm.tools.Predicate;
24import org.openstreetmap.josm.tools.Predicates;
25import org.openstreetmap.josm.tools.Utils;
26
27public abstract class Condition {
28
29 public abstract boolean applies(Environment e);
30
31 public static Condition createKeyValueCondition(String k, String v, Op op, Context context, boolean considerValAsKey) {
32 switch (context) {
33 case PRIMITIVE:
34 if (KeyValueRegexpCondition.SUPPORTED_OPS.contains(op) && !considerValAsKey)
35 return new KeyValueRegexpCondition(k, v, op, false);
36 if (!considerValAsKey && op.equals(Op.EQ))
37 return new SimpleKeyValueCondition(k, v);
38 return new KeyValueCondition(k, v, op, considerValAsKey);
39 case LINK:
40 if (considerValAsKey)
41 throw new MapCSSException("''considerValAsKey'' not supported in LINK context");
42 if ("role".equalsIgnoreCase(k))
43 return new RoleCondition(v, op);
44 else if ("index".equalsIgnoreCase(k))
45 return new IndexCondition(v, op);
46 else
47 throw new MapCSSException(
48 MessageFormat.format("Expected key ''role'' or ''index'' in link context. Got ''{0}''.", k));
49
50 default: throw new AssertionError();
51 }
52 }
53
54 public static Condition createKeyCondition(String k, boolean not, KeyMatchType matchType, Context context) {
55 switch (context) {
56 case PRIMITIVE:
57 return new KeyCondition(k, not, matchType);
58 case LINK:
59 if (matchType != null)
60 throw new MapCSSException("Question mark operator ''?'' and regexp match not supported in LINK context");
61 if (not)
62 return new RoleCondition(k, Op.NEQ);
63 else
64 return new RoleCondition(k, Op.EQ);
65
66 default: throw new AssertionError();
67 }
68 }
69
70 public static PseudoClassCondition createPseudoClassCondition(String id, boolean not, Context context) {
71 return new PseudoClassCondition(id, not, context);
72 }
73
74 public static ClassCondition createClassCondition(String id, boolean not, Context context) {
75 return new ClassCondition(id, not);
76 }
77
78 public static ExpressionCondition createExpressionCondition(Expression e, Context context) {
79 return new ExpressionCondition(e);
80 }
81
82 public static enum Op {
83 EQ, NEQ, GREATER_OR_EQUAL, GREATER, LESS_OR_EQUAL, LESS,
84 REGEX, NREGEX, ONE_OF, BEGINS_WITH, ENDS_WITH, CONTAINS;
85
86 private static final Set<Op> NEGATED_OPS = EnumSet.of(NEQ, NREGEX);
87
88 public boolean eval(String testString, String prototypeString) {
89 if (testString == null && !NEGATED_OPS.contains(this))
90 return false;
91 switch (this) {
92 case EQ:
93 return Objects.equals(testString, prototypeString);
94 case NEQ:
95 return !Objects.equals(testString, prototypeString);
96 case REGEX:
97 case NREGEX:
98 final boolean contains = Pattern.compile(prototypeString).matcher(testString).find();
99 return REGEX.equals(this) ? contains : !contains;
100 case ONE_OF:
101 return Arrays.asList(testString.split("\\s*;\\s*")).contains(prototypeString);
102 case BEGINS_WITH:
103 return testString.startsWith(prototypeString);
104 case ENDS_WITH:
105 return testString.endsWith(prototypeString);
106 case CONTAINS:
107 return testString.contains(prototypeString);
108 }
109
110 float test_float;
111 try {
112 test_float = Float.parseFloat(testString);
113 } catch (NumberFormatException e) {
114 return false;
115 }
116 float prototype_float = Float.parseFloat(prototypeString);
117
118 switch (this) {
119 case GREATER_OR_EQUAL:
120 return test_float >= prototype_float;
121 case GREATER:
122 return test_float > prototype_float;
123 case LESS_OR_EQUAL:
124 return test_float <= prototype_float;
125 case LESS:
126 return test_float < prototype_float;
127 default:
128 throw new AssertionError();
129 }
130 }
131 }
132
133 /**
134 * Context, where the condition applies.
135 */
136 public static enum Context {
137 /**
138 * normal primitive selector, e.g. way[highway=residential]
139 */
140 PRIMITIVE,
141
142 /**
143 * link between primitives, e.g. relation &gt;[role=outer] way
144 */
145 LINK
146 }
147
148 public static final EnumSet<Op> COMPARISON_OPERATERS =
149 EnumSet.of(Op.GREATER_OR_EQUAL, Op.GREATER, Op.LESS_OR_EQUAL, Op.LESS);
150
151 /**
152 * Most common case of a KeyValueCondition.
153 *
154 * Extra class for performance reasons.
155 */
156 public static class SimpleKeyValueCondition extends Condition {
157 public final String k;
158 public final String v;
159
160 public SimpleKeyValueCondition(String k, String v) {
161 this.k = k;
162 this.v = v;
163 }
164
165 @Override
166 public boolean applies(Environment e) {
167 return v.equals(e.osm.get(k));
168 }
169
170 public Tag asTag() {
171 return new Tag(k, v);
172 }
173
174 @Override
175 public String toString() {
176 return '[' + k + '=' + v + ']';
177 }
178
179 }
180
181 /**
182 * <p>Represents a key/value condition which is either applied to a primitive.</p>
183 *
184 */
185 public static class KeyValueCondition extends Condition {
186
187 public final String k;
188 public final String v;
189 public final Op op;
190 public boolean considerValAsKey;
191
192 /**
193 * <p>Creates a key/value-condition.</p>
194 *
195 * @param k the key
196 * @param v the value
197 * @param op the operation
198 * @param considerValAsKey whether to consider {@code v} as another key and compare the values of key {@code k} and key {@code v}.
199 */
200 public KeyValueCondition(String k, String v, Op op, boolean considerValAsKey) {
201 this.k = k;
202 this.v = v;
203 this.op = op;
204 this.considerValAsKey = considerValAsKey;
205 }
206
207 @Override
208 public boolean applies(Environment env) {
209 return op.eval(env.osm.get(k), considerValAsKey ? env.osm.get(v) : v);
210 }
211
212 public Tag asTag() {
213 return new Tag(k, v);
214 }
215
216 @Override
217 public String toString() {
218 return "[" + k + "'" + op + "'" + v + "]";
219 }
220 }
221
222 public static class KeyValueRegexpCondition extends KeyValueCondition {
223
224 public final Pattern pattern;
225 public static final EnumSet<Op> SUPPORTED_OPS = EnumSet.of(Op.REGEX, Op.NREGEX);
226
227 public KeyValueRegexpCondition(String k, String v, Op op, boolean considerValAsKey) {
228 super(k, v, op, considerValAsKey);
229 CheckParameterUtil.ensureThat(!considerValAsKey, "considerValAsKey is not supported");
230 CheckParameterUtil.ensureThat(SUPPORTED_OPS.contains(op), "Op must be REGEX or NREGEX");
231 this.pattern = Pattern.compile(v);
232 }
233
234 @Override
235 public boolean applies(Environment env) {
236 final String value = env.osm.get(k);
237 if (Op.REGEX.equals(op)) {
238 return value != null && pattern.matcher(value).find();
239 } else if (Op.NREGEX.equals(op)) {
240 return value == null || !pattern.matcher(value).find();
241 } else {
242 throw new IllegalStateException();
243 }
244 }
245 }
246
247 public static class RoleCondition extends Condition {
248 public final String role;
249 public final Op op;
250
251 public RoleCondition(String role, Op op) {
252 this.role = role;
253 this.op = op;
254 }
255
256 @Override
257 public boolean applies(Environment env) {
258 String testRole = env.getRole();
259 if (testRole == null) return false;
260 return op.eval(testRole, role);
261 }
262 }
263
264 public static class IndexCondition extends Condition {
265 public final String index;
266 public final Op op;
267
268 public IndexCondition(String index, Op op) {
269 this.index = index;
270 this.op = op;
271 }
272
273 @Override
274 public boolean applies(Environment env) {
275 if (env.index == null) return false;
276 if (index.startsWith("-")) {
277 return env.count != null && op.eval(Integer.toString(env.index - env.count), index);
278 } else {
279 return op.eval(Integer.toString(env.index + 1), index);
280 }
281 }
282 }
283
284 public static enum KeyMatchType {
285 EQ, TRUE, FALSE, REGEX
286 }
287
288 /**
289 * <p>KeyCondition represent one of the following conditions in either the link or the
290 * primitive context:</p>
291 * <pre>
292 * ["a label"] PRIMITIVE: the primitive has a tag "a label"
293 * LINK: the parent is a relation and it has at least one member with the role
294 * "a label" referring to the child
295 *
296 * [!"a label"] PRIMITIVE: the primitive doesn't have a tag "a label"
297 * LINK: the parent is a relation but doesn't have a member with the role
298 * "a label" referring to the child
299 *
300 * ["a label"?] PRIMITIVE: the primitive has a tag "a label" whose value evaluates to a true-value
301 * LINK: not supported
302 *
303 * ["a label"?!] PRIMITIVE: the primitive has a tag "a label" whose value evaluates to a false-value
304 * LINK: not supported
305 * </pre>
306 */
307 public static class KeyCondition extends Condition {
308
309 public final String label;
310 public final boolean negateResult;
311 public final KeyMatchType matchType;
312 public Predicate<String> containsPattern;
313
314 public KeyCondition(String label, boolean negateResult, KeyMatchType matchType){
315 this.label = label;
316 this.negateResult = negateResult;
317 this.matchType = matchType;
318 this.containsPattern = KeyMatchType.REGEX.equals(matchType)
319 ? Predicates.stringContainsPattern(Pattern.compile(label))
320 : null;
321 }
322
323 @Override
324 public boolean applies(Environment e) {
325 switch(e.getContext()) {
326 case PRIMITIVE:
327 if (KeyMatchType.TRUE.equals(matchType))
328 return e.osm.isKeyTrue(label) ^ negateResult;
329 else if (KeyMatchType.FALSE.equals(matchType))
330 return e.osm.isKeyFalse(label) ^ negateResult;
331 else if (KeyMatchType.REGEX.equals(matchType)) {
332 return Utils.exists(e.osm.keySet(), containsPattern) ^ negateResult;
333 } else {
334 return e.osm.hasKey(label) ^ negateResult;
335 }
336 case LINK:
337 Utils.ensure(false, "Illegal state: KeyCondition not supported in LINK context");
338 return false;
339 default: throw new AssertionError();
340 }
341 }
342
343 public Tag asTag(OsmPrimitive p) {
344 String key = label;
345 if (KeyMatchType.REGEX.equals(matchType)) {
346 final Collection<String> matchingKeys = Utils.filter(p.keySet(), containsPattern);
347 if (!matchingKeys.isEmpty()) {
348 key = matchingKeys.iterator().next();
349 }
350 }
351 return new Tag(key, p.get(key));
352 }
353
354 @Override
355 public String toString() {
356 return "[" + (negateResult ? "!" : "") + label + "]";
357 }
358 }
359
360 public static class ClassCondition extends Condition {
361
362 public final String id;
363 public final boolean not;
364
365 public ClassCondition(String id, boolean not) {
366 this.id = id;
367 this.not = not;
368 }
369
370 @Override
371 public boolean applies(Environment env) {
372 return env != null && env.getCascade(env.layer) != null && not ^ env.getCascade(env.layer).containsKey(id);
373 }
374
375 @Override
376 public String toString() {
377 return (not ? "!" : "") + "." + id;
378 }
379 }
380
381 public static class PseudoClassCondition extends Condition {
382
383 public final String id;
384 public final boolean not;
385
386 public PseudoClassCondition(String id, boolean not, Context context) {
387 this.id = id;
388 this.not = not;
389 CheckParameterUtil.ensureThat(!"sameTags".equals(id) || Context.LINK.equals(context), "sameTags only supported in LINK context");
390 }
391
392 @Override
393 public boolean applies(Environment e) {
394 return not ^ appliesImpl(e);
395 }
396
397 public boolean appliesImpl(Environment e) {
398 switch(id) {
399 case "closed":
400 if (e.osm instanceof Way && ((Way) e.osm).isClosed())
401 return true;
402 if (e.osm instanceof Relation && ((Relation) e.osm).isMultipolygon())
403 return true;
404 break;
405 case "modified":
406 return e.osm.isModified() || e.osm.isNewOrUndeleted();
407 case "new":
408 return e.osm.isNew();
409 case "connection":
410 return e.osm instanceof Node && ((Node) e.osm).isConnectionNode();
411 case "tagged":
412 return e.osm.isTagged();
413 case "sameTags":
414 return e.osm.hasSameInterestingTags(Utils.firstNonNull(e.child, e.parent));
415 case "areaStyle":
416 // only for validator
417 return ElemStyles.hasAreaElemStyle(e.osm, false);
418 case "unconnected":
419 return e.osm instanceof Node && OsmPrimitive.getFilteredList(e.osm.getReferrers(), Way.class).isEmpty();
420 case "righthandtraffic":
421 return ExpressionFactory.Functions.is_right_hand_traffic(e);
422 case "unclosed_multipolygon":
423 return e.osm instanceof Relation && ((Relation) e.osm).isMultipolygon() &&
424 !e.osm.isIncomplete() && !((Relation) e.osm).hasIncompleteMembers() &&
425 !MultipolygonCache.getInstance().get(Main.map.mapView, (Relation) e.osm).getOpenEnds().isEmpty();
426 case "open_end":
427 // handling at org.openstreetmap.josm.gui.mappaint.mapcss.Selector.ChildOrParentSelector.MultipolygonOpenEndFinder
428 return true;
429 }
430 return false;
431 }
432
433 @Override
434 public String toString() {
435 return ":" + (not ? "!" : "") + id;
436 }
437 }
438
439 public static class ExpressionCondition extends Condition {
440
441 private final Expression e;
442
443 public ExpressionCondition(Expression e) {
444 this.e = e;
445 }
446
447 @Override
448 public boolean applies(Environment env) {
449 Boolean b = Cascade.convertTo(e.evaluate(env), Boolean.class);
450 return b != null && b;
451 }
452
453 @Override
454 public String toString() {
455 return "[" + e + "]";
456 }
457 }
458}
Note: See TracBrowser for help on using the repository browser.