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

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

fix #10299 - MapCSS index for last element of object

Negative index numbers count from last to first, so >[index=-1] matches the last one.

  • Property svn:eol-style set to native
File size: 15.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.EnumSet;
7import java.util.Objects;
8import java.util.Set;
9import java.util.regex.Pattern;
10
11import org.openstreetmap.josm.data.osm.Node;
12import org.openstreetmap.josm.data.osm.OsmPrimitive;
13import org.openstreetmap.josm.data.osm.Relation;
14import org.openstreetmap.josm.data.osm.Tag;
15import org.openstreetmap.josm.data.osm.Way;
16import org.openstreetmap.josm.gui.mappaint.Cascade;
17import org.openstreetmap.josm.gui.mappaint.ElemStyles;
18import org.openstreetmap.josm.gui.mappaint.Environment;
19import org.openstreetmap.josm.tools.CheckParameterUtil;
20import org.openstreetmap.josm.tools.Predicate;
21import org.openstreetmap.josm.tools.Predicates;
22import org.openstreetmap.josm.tools.Utils;
23
24public abstract class Condition {
25
26 public abstract boolean applies(Environment e);
27
28 public static Condition createKeyValueCondition(String k, String v, Op op, Context context, boolean considerValAsKey) {
29 switch (context) {
30 case PRIMITIVE:
31 if (KeyValueRegexpCondition.SUPPORTED_OPS.contains(op) && !considerValAsKey)
32 return new KeyValueRegexpCondition(k, v, op, false);
33 if (!considerValAsKey && op.equals(Op.EQ))
34 return new SimpleKeyValueCondition(k, v);
35 return new KeyValueCondition(k, v, op, considerValAsKey);
36 case LINK:
37 if (considerValAsKey)
38 throw new MapCSSException("''considerValAsKey'' not supported in LINK context");
39 if ("role".equalsIgnoreCase(k))
40 return new RoleCondition(v, op);
41 else if ("index".equalsIgnoreCase(k))
42 return new IndexCondition(v, op);
43 else
44 throw new MapCSSException(
45 MessageFormat.format("Expected key ''role'' or ''index'' in link context. Got ''{0}''.", k));
46
47 default: throw new AssertionError();
48 }
49 }
50
51 public static Condition createKeyCondition(String k, boolean not, KeyMatchType matchType, Context context) {
52 switch (context) {
53 case PRIMITIVE:
54 return new KeyCondition(k, not, matchType);
55 case LINK:
56 if (matchType != null)
57 throw new MapCSSException("Question mark operator ''?'' and regexp match not supported in LINK context");
58 if (not)
59 return new RoleCondition(k, Op.NEQ);
60 else
61 return new RoleCondition(k, Op.EQ);
62
63 default: throw new AssertionError();
64 }
65 }
66
67 public static PseudoClassCondition createPseudoClassCondition(String id, boolean not, Context context) {
68 return new PseudoClassCondition(id, not, context);
69 }
70
71 public static ClassCondition createClassCondition(String id, boolean not, Context context) {
72 return new ClassCondition(id, not);
73 }
74
75 public static ExpressionCondition createExpressionCondition(Expression e, Context context) {
76 return new ExpressionCondition(e);
77 }
78
79 public static enum Op {
80 EQ, NEQ, GREATER_OR_EQUAL, GREATER, LESS_OR_EQUAL, LESS,
81 REGEX, NREGEX, ONE_OF, BEGINS_WITH, ENDS_WITH, CONTAINS;
82
83 private static final Set<Op> NEGATED_OPS = EnumSet.of(NEQ, NREGEX);
84
85 public boolean eval(String testString, String prototypeString) {
86 if (testString == null && !NEGATED_OPS.contains(this))
87 return false;
88 switch (this) {
89 case EQ:
90 return Objects.equals(testString, prototypeString);
91 case NEQ:
92 return !Objects.equals(testString, prototypeString);
93 case REGEX:
94 case NREGEX:
95 final boolean contains = Pattern.compile(prototypeString).matcher(testString).find();
96 return REGEX.equals(this) ? contains : !contains;
97 case ONE_OF:
98 return Arrays.asList(testString.split("\\s*;\\s*")).contains(prototypeString);
99 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 if (index.startsWith("-")) {
274 return env.count != null && op.eval(Integer.toString(env.index - env.count), index);
275 } else {
276 return op.eval(Integer.toString(env.index + 1), index);
277 }
278 }
279 }
280
281 public static enum KeyMatchType {
282 EQ, TRUE, FALSE, REGEX
283 }
284
285 /**
286 * <p>KeyCondition represent one of the following conditions in either the link or the
287 * primitive context:</p>
288 * <pre>
289 * ["a label"] PRIMITIVE: the primitive has a tag "a label"
290 * LINK: the parent is a relation and it has at least one member with the role
291 * "a label" referring to the child
292 *
293 * [!"a label"] PRIMITIVE: the primitive doesn't have a tag "a label"
294 * LINK: the parent is a relation but doesn't have a member with the role
295 * "a label" referring to the child
296 *
297 * ["a label"?] PRIMITIVE: the primitive has a tag "a label" whose value evaluates to a true-value
298 * LINK: not supported
299 *
300 * ["a label"?!] PRIMITIVE: the primitive has a tag "a label" whose value evaluates to a false-value
301 * LINK: not supported
302 * </pre>
303 */
304 public static class KeyCondition extends Condition {
305
306 public final String label;
307 public final boolean negateResult;
308 public final KeyMatchType matchType;
309 public Predicate<String> containsPattern;
310
311 public KeyCondition(String label, boolean negateResult, KeyMatchType matchType){
312 this.label = label;
313 this.negateResult = negateResult;
314 this.matchType = matchType;
315 this.containsPattern = KeyMatchType.REGEX.equals(matchType)
316 ? Predicates.stringContainsPattern(Pattern.compile(label))
317 : null;
318 }
319
320 @Override
321 public boolean applies(Environment e) {
322 switch(e.getContext()) {
323 case PRIMITIVE:
324 if (KeyMatchType.TRUE.equals(matchType))
325 return e.osm.isKeyTrue(label) ^ negateResult;
326 else if (KeyMatchType.FALSE.equals(matchType))
327 return e.osm.isKeyFalse(label) ^ negateResult;
328 else if (KeyMatchType.REGEX.equals(matchType)) {
329 return Utils.exists(e.osm.keySet(), containsPattern) ^ negateResult;
330 } else {
331 return e.osm.hasKey(label) ^ negateResult;
332 }
333 case LINK:
334 Utils.ensure(false, "Illegal state: KeyCondition not supported in LINK context");
335 return false;
336 default: throw new AssertionError();
337 }
338 }
339
340 public Tag asTag() {
341 return new Tag(label);
342 }
343
344 @Override
345 public String toString() {
346 return "[" + (negateResult ? "!" : "") + label + "]";
347 }
348 }
349
350 public static class ClassCondition extends Condition {
351
352 public final String id;
353 public final boolean not;
354
355 public ClassCondition(String id, boolean not) {
356 this.id = id;
357 this.not = not;
358 }
359
360 @Override
361 public boolean applies(Environment env) {
362 return env != null && env.getCascade(env.layer) != null && not ^ env.getCascade(env.layer).containsKey(id);
363 }
364
365 @Override
366 public String toString() {
367 return (not ? "!" : "") + "." + id;
368 }
369 }
370
371 public static class PseudoClassCondition extends Condition {
372
373 public final String id;
374 public final boolean not;
375
376 public PseudoClassCondition(String id, boolean not, Context context) {
377 this.id = id;
378 this.not = not;
379 CheckParameterUtil.ensureThat(!"sameTags".equals(id) || Context.LINK.equals(context), "sameTags only supported in LINK context");
380 }
381
382 @Override
383 public boolean applies(Environment e) {
384 return not ^ appliesImpl(e);
385 }
386
387 public boolean appliesImpl(Environment e) {
388 switch(id) {
389 case "closed":
390 if (e.osm instanceof Way && ((Way) e.osm).isClosed())
391 return true;
392 if (e.osm instanceof Relation && ((Relation) e.osm).isMultipolygon())
393 return true;
394 break;
395 case "modified":
396 return e.osm.isModified() || e.osm.isNewOrUndeleted();
397 case "new":
398 return e.osm.isNew();
399 case "connection":
400 return e.osm instanceof Node && ((Node) e.osm).isConnectionNode();
401 case "tagged":
402 return e.osm.isTagged();
403 case "sameTags":
404 return e.osm.hasSameInterestingTags(Utils.firstNonNull(e.child, e.parent));
405 case "areaStyle":
406 // only for validator
407 return ElemStyles.hasAreaElemStyle(e.osm, false);
408 case "unconnected":
409 return e.osm instanceof Node && OsmPrimitive.getFilteredList(e.osm.getReferrers(), Way.class).isEmpty();
410 case "righthandtraffic":
411 return ExpressionFactory.Functions.is_right_hand_traffic(e);
412 }
413 return false;
414 }
415
416 @Override
417 public String toString() {
418 return ":" + (not ? "!" : "") + id;
419 }
420 }
421
422 public static class ExpressionCondition extends Condition {
423
424 private final Expression e;
425
426 public ExpressionCondition(Expression e) {
427 this.e = e;
428 }
429
430 @Override
431 public boolean applies(Environment env) {
432 Boolean b = Cascade.convertTo(e.evaluate(env), Boolean.class);
433 return b != null && b;
434 }
435
436 @Override
437 public String toString() {
438 return "[" + e + "]";
439 }
440 }
441}
Note: See TracBrowser for help on using the repository browser.