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

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

fix #9596 - MapCSS style: determine opacity/transparency of a color using alpha()

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