source: josm/trunk/src/org/openstreetmap/josm/gui/mappaint/mapcss/ExpressionFactory.java@ 6560

Last change on this file since 6560 was 6560, checked in by simon04, 10 years ago

see #9485 - MapCSS: add support for set class_name instruction and .class_name condition (which is specified in the MapCSS specification)

File size: 24.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.mappaint.mapcss;
3
4import static org.openstreetmap.josm.tools.Utils.equal;
5
6import java.awt.Color;
7import java.lang.reflect.Array;
8import java.lang.reflect.InvocationTargetException;
9import java.lang.reflect.Method;
10import java.util.ArrayList;
11import java.util.Arrays;
12import java.util.List;
13import java.util.regex.Matcher;
14import java.util.regex.Pattern;
15
16import org.openstreetmap.josm.Main;
17import org.openstreetmap.josm.actions.search.SearchCompiler;
18import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
19import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
20import org.openstreetmap.josm.data.osm.OsmPrimitive;
21import org.openstreetmap.josm.gui.mappaint.Cascade;
22import org.openstreetmap.josm.gui.mappaint.Environment;
23import org.openstreetmap.josm.tools.ColorHelper;
24import org.openstreetmap.josm.tools.Utils;
25
26/**
27 * Factory to generate Expressions.
28 *
29 * See {@link #createFunctionExpression}.
30 */
31public final class ExpressionFactory {
32
33 private static final List<Method> arrayFunctions;
34 private static final List<Method> parameterFunctions;
35 private static final Functions FUNCTIONS_INSTANCE = new Functions();
36
37 static {
38 arrayFunctions = new ArrayList<Method>();
39 parameterFunctions = new ArrayList<Method>();
40 for (Method m : Functions.class.getDeclaredMethods()) {
41 Class<?>[] paramTypes = m.getParameterTypes();
42 if (paramTypes.length == 1 && paramTypes[0].isArray()) {
43 arrayFunctions.add(m);
44 } else {
45 parameterFunctions.add(m);
46 }
47 }
48 try {
49 parameterFunctions.add(Math.class.getMethod("abs", float.class));
50 parameterFunctions.add(Math.class.getMethod("acos", double.class));
51 parameterFunctions.add(Math.class.getMethod("asin", double.class));
52 parameterFunctions.add(Math.class.getMethod("atan", double.class));
53 parameterFunctions.add(Math.class.getMethod("atan2", double.class, double.class));
54 parameterFunctions.add(Math.class.getMethod("ceil", double.class));
55 parameterFunctions.add(Math.class.getMethod("cos", double.class));
56 parameterFunctions.add(Math.class.getMethod("cosh", double.class));
57 parameterFunctions.add(Math.class.getMethod("exp", double.class));
58 parameterFunctions.add(Math.class.getMethod("floor", double.class));
59 parameterFunctions.add(Math.class.getMethod("log", double.class));
60 parameterFunctions.add(Math.class.getMethod("max", float.class, float.class));
61 parameterFunctions.add(Math.class.getMethod("min", float.class, float.class));
62 parameterFunctions.add(Math.class.getMethod("random"));
63 parameterFunctions.add(Math.class.getMethod("round", float.class));
64 parameterFunctions.add(Math.class.getMethod("signum", double.class));
65 parameterFunctions.add(Math.class.getMethod("sin", double.class));
66 parameterFunctions.add(Math.class.getMethod("sinh", double.class));
67 parameterFunctions.add(Math.class.getMethod("sqrt", double.class));
68 parameterFunctions.add(Math.class.getMethod("tan", double.class));
69 parameterFunctions.add(Math.class.getMethod("tanh", double.class));
70 } catch (NoSuchMethodException ex) {
71 throw new RuntimeException(ex);
72 } catch (SecurityException ex) {
73 throw new RuntimeException(ex);
74 }
75 }
76
77 private ExpressionFactory() {
78 // Hide default constructor for utils classes
79 }
80
81 @SuppressWarnings("UnusedDeclaration")
82 public static class Functions {
83
84 Environment env;
85
86 /**
87 * Identity function for compatibility with MapCSS specification.
88 * @param o any object
89 * @return {@code o} unchanged
90 */
91 public static Object eval(Object o) {
92 return o;
93 }
94
95 public static float plus(float... args) {
96 float res = 0;
97 for (float f : args) {
98 res += f;
99 }
100 return res;
101 }
102
103 public static Float minus(float... args) {
104 if (args.length == 0) {
105 return 0.0F;
106 }
107 if (args.length == 1) {
108 return -args[0];
109 }
110 float res = args[0];
111 for (int i = 1; i < args.length; ++i) {
112 res -= args[i];
113 }
114 return res;
115 }
116
117 public static float times(float... args) {
118 float res = 1;
119 for (float f : args) {
120 res *= f;
121 }
122 return res;
123 }
124
125 public static Float divided_by(float... args) {
126 if (args.length == 0) {
127 return 1.0F;
128 }
129 float res = args[0];
130 for (int i = 1; i < args.length; ++i) {
131 if (args[i] == 0.0F) {
132 return null;
133 }
134 res /= args[i];
135 }
136 return res;
137 }
138
139 /**
140 * Creates a list of values, e.g., for the {@code dashes} property.
141 * @see {@link Arrays#asList(Object[])}
142 */
143 public static List list(Object... args) {
144 return Arrays.asList(args);
145 }
146
147 /**
148 * Get the {@code n}th element of the list {@code lst} (counting starts at 0).
149 * @since 5699
150 */
151 public static Object get(List<?> lst, float n) {
152 int idx = Math.round(n);
153 if (idx >= 0 && idx < lst.size()) {
154 return lst.get(idx);
155 }
156 return null;
157 }
158
159 /**
160 * Splits string {@code toSplit} at occurrences of the separator string {@code sep} and returns a list of matches.
161 * @see {@link String#split(String)}
162 * @since 5699
163 */
164 public static List<String> split(String sep, String toSplit) {
165 return Arrays.asList(toSplit.split(Pattern.quote(sep), -1));
166 }
167
168 /**
169 * Creates a color value with the specified amounts of {@code r}ed, {@code g}reen, {@code b}lue (arguments from 0.0 to 1.0)
170 * @see {@link Color#Color(float, float, float)}
171 */
172 public static Color rgb(float r, float g, float b) {
173 Color c;
174 try {
175 c = new Color(r, g, b);
176 } catch (IllegalArgumentException e) {
177 return null;
178 }
179 return c;
180 }
181
182 /**
183 * Creates a color value from an HTML notation, i.e., {@code #rrggbb}.
184 */
185 public static Color html2color(String html) {
186 return ColorHelper.html2color(html);
187 }
188
189 /**
190 * Computes the HTML notation ({@code #rrggbb}) for a color value).
191 */
192 public static String color2html(Color c) {
193 return ColorHelper.color2html(c);
194 }
195
196 /**
197 * Get the value of the red color channel in the rgb color model
198 * @see {@link java.awt.Color#getRed()}
199 */
200 public static float red(Color c) {
201 return Utils.color_int2float(c.getRed());
202 }
203
204 /**
205 * Get the value of the green color channel in the rgb color model
206 * @see {@link java.awt.Color#getGreen()}
207 */
208 public static float green(Color c) {
209 return Utils.color_int2float(c.getGreen());
210 }
211
212 /**
213 * Get the value of the blue color channel in the rgb color model
214 * @see {@link java.awt.Color#getBlue()}
215 */
216 public static float blue(Color c) {
217 return Utils.color_int2float(c.getBlue());
218 }
219
220 /**
221 * Assembles the strings to one.
222 */
223 public static String concat(Object... args) {
224 StringBuilder res = new StringBuilder();
225 for (Object f : args) {
226 res.append(f.toString());
227 }
228 return res.toString();
229 }
230
231 /**
232 * Returns the value of the property {@code key}, e.g., {@code prop("width")}.
233 */
234 public Object prop(String key) {
235 return prop(key, null);
236 }
237
238 /**
239 * Returns the value of the property {@code key} from layer {@code layer}.
240 */
241 public Object prop(String key, String layer) {
242 Cascade c;
243 if (layer == null) {
244 c = env.mc.getCascade(env.layer);
245 } else {
246 c = env.mc.getCascade(layer);
247 }
248 return c.get(key);
249 }
250
251 /**
252 * Determines whether property {@code key} is set.
253 */
254 public Boolean is_prop_set(String key) {
255 return is_prop_set(key, null);
256 }
257
258 /**
259 * Determines whether property {@code key} is set on layer {@code layer}.
260 */
261 public Boolean is_prop_set(String key, String layer) {
262 Cascade c;
263 if (layer == null) {
264 // env.layer is null if expression is evaluated
265 // in ExpressionCondition, but MultiCascade.getCascade
266 // handles this
267 c = env.mc.getCascade(env.layer);
268 } else {
269 c = env.mc.getCascade(layer);
270 }
271 return c.containsKey(key);
272 }
273
274 /**
275 * Gets the value of the key {@code key} from the object in question.
276 */
277 public String tag(String key) {
278 return env.osm == null ? null : env.osm.get(key);
279 }
280
281 /**
282 * Gets the first non-null value of the key {@code key} from the object's parent(s).
283 */
284 public String parent_tag(String key) {
285 if (env.parent == null) {
286 if (env.osm != null) {
287 // we don't have a matched parent, so just search all referrers
288 for (OsmPrimitive parent : env.osm.getReferrers()) {
289 String value = parent.get(key);
290 if (value != null) {
291 return value;
292 }
293 }
294 }
295 return null;
296 }
297 return env.parent.get(key);
298 }
299
300 /**
301 * Determines whether the object has a tag with the given key.
302 */
303 public boolean has_tag_key(String key) {
304 return env.osm.hasKey(key);
305 }
306
307 /**
308 * Returns the index of node in parent way or member in parent relation.
309 */
310 public Float index() {
311 if (env.index == null) {
312 return null;
313 }
314 return new Float(env.index + 1);
315 }
316
317 public String role() {
318 return env.getRole();
319 }
320
321 public static boolean not(boolean b) {
322 return !b;
323 }
324
325 public static boolean greater_equal(float a, float b) {
326 return a >= b;
327 }
328
329 public static boolean less_equal(float a, float b) {
330 return a <= b;
331 }
332
333 public static boolean greater(float a, float b) {
334 return a > b;
335 }
336
337 public static boolean less(float a, float b) {
338 return a < b;
339 }
340
341 /**
342 * Determines if the objects {@code a} and {@code b} are equal.
343 * @see {@link Object#equals(Object)}
344 */
345 public static boolean equal(Object a, Object b) {
346 // make sure the casts are done in a meaningful way, so
347 // the 2 objects really can be considered equal
348 for (Class<?> klass : new Class[]{Float.class, Boolean.class, Color.class, float[].class, String.class}) {
349 Object a2 = Cascade.convertTo(a, klass);
350 Object b2 = Cascade.convertTo(b, klass);
351 if (a2 != null && b2 != null && a2.equals(b2)) {
352 return true;
353 }
354 }
355 return false;
356 }
357
358 /**
359 * Determines whether the JOSM search with {@code searchStr} applies to the object.
360 */
361 public Boolean JOSM_search(String searchStr) {
362 Match m;
363 try {
364 m = SearchCompiler.compile(searchStr, false, false);
365 } catch (ParseError ex) {
366 return null;
367 }
368 return m.match(env.osm);
369 }
370
371 /**
372 * Obtains the JOSM'key {@link org.openstreetmap.josm.data.Preferences} string for key {@code key},
373 * and defaults to {@code def} if that is null.
374 * @see {@link org.openstreetmap.josm.data.Preferences#get(String, String)}
375 */
376 public static String JOSM_pref(String key, String def) {
377 String res = Main.pref.get(key, null);
378 return res != null ? res : def;
379 }
380
381 /**
382 * Obtains the JOSM'key {@link org.openstreetmap.josm.data.Preferences} color for key {@code key},
383 * and defaults to {@code def} if that is null.
384 * @see {@link org.openstreetmap.josm.data.Preferences#getColor(String, java.awt.Color)}
385 */
386 public static Color JOSM_pref_color(String key, Color def) {
387 Color res = Main.pref.getColor(key, null);
388 return res != null ? res : def;
389 }
390
391 /**
392 * Tests if string {@code target} matches pattern {@code pattern}
393 * @see {@link Pattern#matches(String, CharSequence)}
394 * @since 5699
395 */
396 public static boolean regexp_test(String pattern, String target) {
397 return Pattern.matches(pattern, target);
398 }
399
400 /**
401 * Tests if string {@code target} matches pattern {@code pattern}
402 * @param flags a string that may contain "i" (case insensitive), "m" (multiline) and "s" ("dot all")
403 * @since 5699
404 */
405 public static boolean regexp_test(String pattern, String target, String flags) {
406 int f = 0;
407 if (flags.contains("i")) {
408 f |= Pattern.CASE_INSENSITIVE;
409 }
410 if (flags.contains("s")) {
411 f |= Pattern.DOTALL;
412 }
413 if (flags.contains("m")) {
414 f |= Pattern.MULTILINE;
415 }
416 return Pattern.compile(pattern, f).matcher(target).matches();
417 }
418
419 /**
420 * Tries to match string against pattern regexp and returns a list of capture groups in case of success.
421 * The first element (index 0) is the complete match (i.e. string).
422 * Further elements correspond to the bracketed parts of the regular expression.
423 * @param flags a string that may contain "i" (case insensitive), "m" (multiline) and "s" ("dot all")
424 * @since 5701
425 */
426 public static List<String> regexp_match(String pattern, String target, String flags) {
427 int f = 0;
428 if (flags.contains("i")) {
429 f |= Pattern.CASE_INSENSITIVE;
430 }
431 if (flags.contains("s")) {
432 f |= Pattern.DOTALL;
433 }
434 if (flags.contains("m")) {
435 f |= Pattern.MULTILINE;
436 }
437 Matcher m = Pattern.compile(pattern, f).matcher(target);
438 return Utils.getMatches(m);
439 }
440
441 /**
442 * Tries to match string against pattern regexp and returns a list of capture groups in case of success.
443 * The first element (index 0) is the complete match (i.e. string).
444 * Further elements correspond to the bracketed parts of the regular expression.
445 * @since 5701
446 */
447 public static List<String> regexp_match(String pattern, String target) {
448 Matcher m = Pattern.compile(pattern).matcher(target);
449 return Utils.getMatches(m);
450 }
451
452 /**
453 * Returns the OSM id of the current object.
454 * @see {@link org.openstreetmap.josm.data.osm.AbstractPrimitive#generateUniqueId()}
455 */
456 public long osm_id() {
457 return env.osm.getUniqueId();
458 }
459
460 /**
461 * Translates some text for the current locale. The first argument is the text to translate,
462 * and the subsequent arguments are parameters for the string indicated by {@code {0}}, {@code {1}}, …
463 */
464 public static String tr(String... args) {
465 final String text = args[0];
466 System.arraycopy(args, 1, args, 0, args.length - 1);
467 return org.openstreetmap.josm.tools.I18n.tr(text, args);
468 }
469
470 /**
471 * Returns the substring of {@code s} starting at index {@code begin} (inclusive, 0-indexed).
472 * * @see {@link String#substring(int)}
473 */
474 public static String substring(String s, /* due to missing Cascade.convertTo for int*/ float begin) {
475 return s == null ? null : s.substring((int) begin);
476 }
477
478 /**
479 * Returns the substring of {@code s} starting at index {@code begin} (inclusive)
480 * and ending at index {@code end}, (exclusive, 0-indexed).
481 * @see {@link String#substring(int, int)}
482 */
483 public static String substring(String s, float begin, float end) {
484 return s == null ? null : s.substring((int) begin, (int) end);
485 }
486
487 /**
488 * Replaces in {@code s} every {@code} target} substring by {@code replacement}.
489 * * @see {@link String#replace(CharSequence, CharSequence)}
490 */
491 public static String replace(String s, String target, String replacement) {
492 return s == null ? null : s.replace(target, replacement);
493 }
494 }
495
496 /**
497 * Main method to create an function-like expression.
498 *
499 * @param name the name of the function or operator
500 * @param args the list of arguments (as expressions)
501 * @return the generated Expression. If no suitable function can be found,
502 * returns {@link NullExpression#INSTANCE}.
503 */
504 public static Expression createFunctionExpression(String name, List<Expression> args) {
505 if (equal(name, "cond") && args.size() == 3)
506 return new CondOperator(args.get(0), args.get(1), args.get(2));
507 else if (equal(name, "and"))
508 return new AndOperator(args);
509 else if (equal(name, "or"))
510 return new OrOperator(args);
511 else if (equal(name, "length") && args.size() == 1)
512 return new LengthFunction(args.get(0));
513
514 for (Method m : arrayFunctions) {
515 if (m.getName().equals(name))
516 return new ArrayFunction(m, args);
517 }
518 for (Method m : parameterFunctions) {
519 if (m.getName().equals(name) && args.size() == m.getParameterTypes().length)
520 return new ParameterFunction(m, args);
521 }
522 return NullExpression.INSTANCE;
523 }
524
525 /**
526 * Expression that always evaluates to null.
527 */
528 public static class NullExpression implements Expression {
529
530 final public static NullExpression INSTANCE = new NullExpression();
531
532 @Override
533 public Object evaluate(Environment env) {
534 return null;
535 }
536 }
537
538 /**
539 * Conditional operator.
540 */
541 public static class CondOperator implements Expression {
542
543 private Expression condition, firstOption, secondOption;
544
545 public CondOperator(Expression condition, Expression firstOption, Expression secondOption) {
546 this.condition = condition;
547 this.firstOption = firstOption;
548 this.secondOption = secondOption;
549 }
550
551 @Override
552 public Object evaluate(Environment env) {
553 Boolean b = Cascade.convertTo(condition.evaluate(env), boolean.class);
554 if (b != null && b)
555 return firstOption.evaluate(env);
556 else
557 return secondOption.evaluate(env);
558 }
559 }
560
561 public static class AndOperator implements Expression {
562
563 private List<Expression> args;
564
565 public AndOperator(List<Expression> args) {
566 this.args = args;
567 }
568
569 @Override
570 public Object evaluate(Environment env) {
571 for (Expression arg : args) {
572 Boolean b = Cascade.convertTo(arg.evaluate(env), boolean.class);
573 if (b == null || !b) {
574 return false;
575 }
576 }
577 return true;
578 }
579 }
580
581 public static class OrOperator implements Expression {
582
583 private List<Expression> args;
584
585 public OrOperator(List<Expression> args) {
586 this.args = args;
587 }
588
589 @Override
590 public Object evaluate(Environment env) {
591 for (Expression arg : args) {
592 Boolean b = Cascade.convertTo(arg.evaluate(env), boolean.class);
593 if (b != null && b) {
594 return true;
595 }
596 }
597 return false;
598 }
599 }
600
601 /**
602 * Function to calculate the length of a string or list in a MapCSS eval
603 * expression.
604 *
605 * Separate implementation to support overloading for different
606 * argument types.
607 */
608 public static class LengthFunction implements Expression {
609
610 private Expression arg;
611
612 public LengthFunction(Expression args) {
613 this.arg = args;
614 }
615
616 @Override
617 public Object evaluate(Environment env) {
618 List<?> l = Cascade.convertTo(arg.evaluate(env), List.class);
619 if (l != null)
620 return l.size();
621 String s = Cascade.convertTo(arg.evaluate(env), String.class);
622 if (s != null)
623 return s.length();
624 return null;
625 }
626 }
627
628 /**
629 * Function that takes a certain number of argument with specific type.
630 *
631 * Implementation is based on a Method object.
632 * If any of the arguments evaluate to null, the result will also be null.
633 */
634 public static class ParameterFunction implements Expression {
635
636 private final Method m;
637 private final List<Expression> args;
638 private final Class<?>[] expectedParameterTypes;
639
640 public ParameterFunction(Method m, List<Expression> args) {
641 this.m = m;
642 this.args = args;
643 expectedParameterTypes = m.getParameterTypes();
644 }
645
646 @Override
647 public Object evaluate(Environment env) {
648 FUNCTIONS_INSTANCE.env = env;
649 Object[] convertedArgs = new Object[expectedParameterTypes.length];
650 for (int i = 0; i < args.size(); ++i) {
651 convertedArgs[i] = Cascade.convertTo(args.get(i).evaluate(env), expectedParameterTypes[i]);
652 if (convertedArgs[i] == null) {
653 return null;
654 }
655 }
656 Object result = null;
657 try {
658 result = m.invoke(FUNCTIONS_INSTANCE, convertedArgs);
659 } catch (IllegalAccessException ex) {
660 throw new RuntimeException(ex);
661 } catch (IllegalArgumentException ex) {
662 throw new RuntimeException(ex);
663 } catch (InvocationTargetException ex) {
664 Main.error(ex);
665 return null;
666 }
667 return result;
668 }
669 }
670
671 /**
672 * Function that takes an arbitrary number of arguments.
673 *
674 * Currently, all array functions are static, so there is no need to
675 * provide the environment, like it is done in {@link ParameterFunction}.
676 * If any of the arguments evaluate to null, the result will also be null.
677 */
678 public static class ArrayFunction implements Expression {
679
680 private final Method m;
681 private final List<Expression> args;
682 private final Class<?> arrayComponentType;
683 private final Object[] convertedArgs;
684
685 public ArrayFunction(Method m, List<Expression> args) {
686 this.m = m;
687 this.args = args;
688 Class<?>[] expectedParameterTypes = m.getParameterTypes();
689 convertedArgs = new Object[expectedParameterTypes.length];
690 arrayComponentType = expectedParameterTypes[0].getComponentType();
691 }
692
693 @Override
694 public Object evaluate(Environment env) {
695 Object arrayArg = Array.newInstance(arrayComponentType, args.size());
696 for (int i = 0; i < args.size(); ++i) {
697 Object o = Cascade.convertTo(args.get(i).evaluate(env), arrayComponentType);
698 if (o == null) {
699 return null;
700 }
701 Array.set(arrayArg, i, o);
702 }
703 convertedArgs[0] = arrayArg;
704
705 Object result = null;
706 try {
707 result = m.invoke(null, convertedArgs);
708 } catch (IllegalAccessException ex) {
709 throw new RuntimeException(ex);
710 } catch (IllegalArgumentException ex) {
711 throw new RuntimeException(ex);
712 } catch (InvocationTargetException ex) {
713 Main.error(ex);
714 return null;
715 }
716 return result;
717 }
718 }
719
720}
Note: See TracBrowser for help on using the repository browser.