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

Last change on this file since 8256 was 8256, checked in by bastiK, 9 years ago

mapcss: improve expression parsing
Now it respects operator precedence (C/C++ style) and allows
mix of plus and minus like this:

3 + 2 * 5 - 2 + 10

This is parsed as

(((3 + (2 * 5)) - 2) + 10)

  • Property svn:eol-style set to native
File size: 44.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.mappaint.mapcss;
3
4import java.awt.Color;
5import java.io.UnsupportedEncodingException;
6import java.lang.annotation.ElementType;
7import java.lang.annotation.Retention;
8import java.lang.annotation.RetentionPolicy;
9import java.lang.annotation.Target;
10import java.lang.reflect.Array;
11import java.lang.reflect.InvocationTargetException;
12import java.lang.reflect.Method;
13import java.net.URLEncoder;
14import java.nio.charset.StandardCharsets;
15import java.util.ArrayList;
16import java.util.Arrays;
17import java.util.Collection;
18import java.util.Collections;
19import java.util.List;
20import java.util.regex.Matcher;
21import java.util.regex.Pattern;
22import java.util.zip.CRC32;
23
24import org.openstreetmap.josm.Main;
25import org.openstreetmap.josm.actions.search.SearchCompiler;
26import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
27import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
28import org.openstreetmap.josm.data.osm.Node;
29import org.openstreetmap.josm.data.osm.OsmPrimitive;
30import org.openstreetmap.josm.data.osm.Way;
31import org.openstreetmap.josm.gui.mappaint.Cascade;
32import org.openstreetmap.josm.gui.mappaint.Environment;
33import org.openstreetmap.josm.gui.util.RotationAngle;
34import org.openstreetmap.josm.io.XmlWriter;
35import org.openstreetmap.josm.tools.ColorHelper;
36import org.openstreetmap.josm.tools.Geometry;
37import org.openstreetmap.josm.tools.Predicates;
38import org.openstreetmap.josm.tools.RightAndLefthandTraffic;
39import org.openstreetmap.josm.tools.Utils;
40
41/**
42 * Factory to generate Expressions.
43 *
44 * See {@link #createFunctionExpression}.
45 */
46public final class ExpressionFactory {
47
48 /**
49 * Marks functions which should be executed also when one or more arguments are null.
50 */
51 @Target(ElementType.METHOD)
52 @Retention(RetentionPolicy.RUNTIME)
53 static @interface NullableArguments {}
54
55 private static final List<Method> arrayFunctions = new ArrayList<>();
56 private static final List<Method> parameterFunctions = new ArrayList<>();
57 private static final List<Method> parameterFunctionsEnv = new ArrayList<>();
58
59 static {
60 for (Method m : Functions.class.getDeclaredMethods()) {
61 Class<?>[] paramTypes = m.getParameterTypes();
62 if (paramTypes.length == 1 && paramTypes[0].isArray()) {
63 arrayFunctions.add(m);
64 } else if (paramTypes.length >= 1 && paramTypes[0].equals(Environment.class)) {
65 parameterFunctionsEnv.add(m);
66 } else {
67 parameterFunctions.add(m);
68 }
69 }
70 try {
71 parameterFunctions.add(Math.class.getMethod("abs", float.class));
72 parameterFunctions.add(Math.class.getMethod("acos", double.class));
73 parameterFunctions.add(Math.class.getMethod("asin", double.class));
74 parameterFunctions.add(Math.class.getMethod("atan", double.class));
75 parameterFunctions.add(Math.class.getMethod("atan2", double.class, double.class));
76 parameterFunctions.add(Math.class.getMethod("ceil", double.class));
77 parameterFunctions.add(Math.class.getMethod("cos", double.class));
78 parameterFunctions.add(Math.class.getMethod("cosh", double.class));
79 parameterFunctions.add(Math.class.getMethod("exp", double.class));
80 parameterFunctions.add(Math.class.getMethod("floor", double.class));
81 parameterFunctions.add(Math.class.getMethod("log", double.class));
82 parameterFunctions.add(Math.class.getMethod("max", float.class, float.class));
83 parameterFunctions.add(Math.class.getMethod("min", float.class, float.class));
84 parameterFunctions.add(Math.class.getMethod("random"));
85 parameterFunctions.add(Math.class.getMethod("round", float.class));
86 parameterFunctions.add(Math.class.getMethod("signum", double.class));
87 parameterFunctions.add(Math.class.getMethod("sin", double.class));
88 parameterFunctions.add(Math.class.getMethod("sinh", double.class));
89 parameterFunctions.add(Math.class.getMethod("sqrt", double.class));
90 parameterFunctions.add(Math.class.getMethod("tan", double.class));
91 parameterFunctions.add(Math.class.getMethod("tanh", double.class));
92 } catch (NoSuchMethodException | SecurityException ex) {
93 throw new RuntimeException(ex);
94 }
95 }
96
97 private ExpressionFactory() {
98 // Hide default constructor for utils classes
99 }
100
101 /**
102 * List of functions that can be used in MapCSS expressions.
103 *
104 * First parameter can be of type {@link Environment} (if needed). This is
105 * automatically filled in by JOSM and the user only sees the remaining
106 * arguments.
107 * When one of the user supplied arguments cannot be converted the
108 * expected type or is null, the function is not called and it returns null
109 * immediately. Add the annotation {@link NullableArguments} to allow
110 * null arguments.
111 * Every method must be static.
112 */
113 @SuppressWarnings("UnusedDeclaration")
114 public static class Functions {
115
116 /**
117 * Identity function for compatibility with MapCSS specification.
118 * @param o any object
119 * @return {@code o} unchanged
120 */
121 public static Object eval(Object o) {
122 return o;
123 }
124
125 /**
126 * Function associated to the numeric "+" operator.
127 * @param args arguments
128 * @return Sum of arguments
129 */
130 public static float plus(float... args) {
131 float res = 0;
132 for (float f : args) {
133 res += f;
134 }
135 return res;
136 }
137
138 /**
139 * Function associated to the numeric "-" operator.
140 * @param args arguments
141 * @return Substraction of arguments
142 */
143 public static Float minus(float... args) {
144 if (args.length == 0) {
145 return 0.0F;
146 }
147 if (args.length == 1) {
148 return -args[0];
149 }
150 float res = args[0];
151 for (int i = 1; i < args.length; ++i) {
152 res -= args[i];
153 }
154 return res;
155 }
156
157 /**
158 * Function associated to the numeric "*" operator.
159 * @param args arguments
160 * @return Multiplication of arguments
161 */
162 public static float times(float... args) {
163 float res = 1;
164 for (float f : args) {
165 res *= f;
166 }
167 return res;
168 }
169
170 /**
171 * Function associated to the numeric "/" operator.
172 * @param args arguments
173 * @return Division of arguments
174 */
175 public static Float divided_by(float... args) {
176 if (args.length == 0) {
177 return 1.0F;
178 }
179 float res = args[0];
180 for (int i = 1; i < args.length; ++i) {
181 if (args[i] == 0.0F) {
182 return null;
183 }
184 res /= args[i];
185 }
186 return res;
187 }
188
189 /**
190 * Creates a list of values, e.g., for the {@code dashes} property.
191 * @param args The values to put in a list
192 * @return list of values
193 * @see Arrays#asList(Object[])
194 */
195 public static List<Object> list(Object... args) {
196 return Arrays.asList(args);
197 }
198
199 /**
200 * Returns the number of elements in a list.
201 * @param lst the list
202 * @return length of the list
203 */
204 public static Integer count(List<?> lst) {
205 return lst.size();
206 }
207
208 /**
209 * Returns the first non-null object. The name originates from the {@code COALESCE} SQL function.
210 * @param args arguments
211 * @return the first non-null object
212 * @deprecated Deprecated in favour of {@link #any(Object...)} from the MapCSS standard.
213 */
214 @NullableArguments
215 @Deprecated
216 public static Object coalesce(Object... args) {
217 return any(args);
218 }
219
220 /**
221 * Returns the first non-null object.
222 * The name originates from <a href="http://wiki.openstreetmap.org/wiki/MapCSS/0.2/eval">MapCSS standard</a>.
223 * @param args arguments
224 * @return the first non-null object
225 * @see #coalesce(Object...)
226 * @see Utils#firstNonNull(Object[])
227 */
228 @NullableArguments
229 public static Object any(Object... args) {
230 return Utils.firstNonNull(args);
231 }
232
233 /**
234 * Get the {@code n}th element of the list {@code lst} (counting starts at 0).
235 * @param lst list
236 * @param n index
237 * @return {@code n}th element of the list, or {@code null} if index out of range
238 * @since 5699
239 */
240 public static Object get(List<?> lst, float n) {
241 int idx = Math.round(n);
242 if (idx >= 0 && idx < lst.size()) {
243 return lst.get(idx);
244 }
245 return null;
246 }
247
248 /**
249 * Splits string {@code toSplit} at occurrences of the separator string {@code sep} and returns a list of matches.
250 * @param sep separator string
251 * @param toSplit string to split
252 * @return list of matches
253 * @see String#split(String)
254 * @since 5699
255 */
256 public static List<String> split(String sep, String toSplit) {
257 return Arrays.asList(toSplit.split(Pattern.quote(sep), -1));
258 }
259
260 /**
261 * 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)
262 * @param r the red component
263 * @param g the green component
264 * @param b the blue component
265 * @return color matching the given components
266 * @see Color#Color(float, float, float)
267 */
268 public static Color rgb(float r, float g, float b) {
269 try {
270 return new Color(r, g, b);
271 } catch (IllegalArgumentException e) {
272 return null;
273 }
274 }
275
276 /**
277 * Creates a color value with the specified amounts of {@code r}ed, {@code g}reen, {@code b}lue, {@code alpha} (arguments from 0.0 to 1.0)
278 * @param r the red component
279 * @param g the green component
280 * @param b the blue component
281 * @param alpha the alpha component
282 * @return color matching the given components
283 * @see Color#Color(float, float, float, float)
284 */
285 public static Color rgba(float r, float g, float b, float alpha) {
286 try {
287 return new Color(r, g, b, alpha);
288 } catch (IllegalArgumentException e) {
289 return null;
290 }
291 }
292
293 /**
294 * Create color from hsb color model. (arguments form 0.0 to 1.0)
295 * @param h hue
296 * @param s saturation
297 * @param b brightness
298 * @return the corresponding color
299 */
300 public static Color hsb_color(float h, float s, float b) {
301 try {
302 return Color.getHSBColor(h, s, b);
303 } catch (IllegalArgumentException e) {
304 return null;
305 }
306 }
307
308 /**
309 * Creates a color value from an HTML notation, i.e., {@code #rrggbb}.
310 * @param html HTML notation
311 * @return color matching the given notation
312 */
313 public static Color html2color(String html) {
314 return ColorHelper.html2color(html);
315 }
316
317 /**
318 * Computes the HTML notation ({@code #rrggbb}) for a color value).
319 * @param c color
320 * @return HTML notation matching the given color
321 */
322 public static String color2html(Color c) {
323 return ColorHelper.color2html(c);
324 }
325
326 /**
327 * Get the value of the red color channel in the rgb color model
328 * @param c color
329 * @return the red color channel in the range [0;1]
330 * @see java.awt.Color#getRed()
331 */
332 public static float red(Color c) {
333 return Utils.color_int2float(c.getRed());
334 }
335
336 /**
337 * Get the value of the green color channel in the rgb color model
338 * @param c color
339 * @return the green color channel in the range [0;1]
340 * @see java.awt.Color#getGreen()
341 */
342 public static float green(Color c) {
343 return Utils.color_int2float(c.getGreen());
344 }
345
346 /**
347 * Get the value of the blue color channel in the rgb color model
348 * @param c color
349 * @return the blue color channel in the range [0;1]
350 * @see java.awt.Color#getBlue()
351 */
352 public static float blue(Color c) {
353 return Utils.color_int2float(c.getBlue());
354 }
355
356 /**
357 * Get the value of the alpha channel in the rgba color model
358 * @param c color
359 * @return the alpha channel in the range [0;1]
360 * @see java.awt.Color#getAlpha()
361 */
362 public static float alpha(Color c) {
363 return Utils.color_int2float(c.getAlpha());
364 }
365
366 /**
367 * Assembles the strings to one.
368 * @param args arguments
369 * @return assembled string
370 * @see Utils#join
371 */
372 @NullableArguments
373 public static String concat(Object... args) {
374 return Utils.join("", Arrays.asList(args));
375 }
376
377 /**
378 * Assembles the strings to one, where the first entry is used as separator.
379 * @param args arguments. First one is used as separator
380 * @return assembled string
381 * @see Utils#join
382 */
383 @NullableArguments
384 public static String join(String... args) {
385 return Utils.join(args[0], Arrays.asList(args).subList(1, args.length));
386 }
387
388 /**
389 * Returns the value of the property {@code key}, e.g., {@code prop("width")}.
390 * @param env the environment
391 * @param key the property key
392 * @return the property value
393 */
394 public static Object prop(final Environment env, String key) {
395 return prop(env, key, null);
396 }
397
398 /**
399 * Returns the value of the property {@code key} from layer {@code layer}.
400 * @param env the environment
401 * @param key the property key
402 * @return the property value
403 */
404 public static Object prop(final Environment env, String key, String layer) {
405 return env.getCascade(layer).get(key);
406 }
407
408 /**
409 * Determines whether property {@code key} is set.
410 * @param env the environment
411 * @param key the property key
412 * @return {@code true} if the property is set, {@code false} otherwise
413 */
414 public static Boolean is_prop_set(final Environment env, String key) {
415 return is_prop_set(env, key, null);
416 }
417
418 /**
419 * Determines whether property {@code key} is set on layer {@code layer}.
420 * @param env the environment
421 * @param key the property key
422 * @return {@code true} if the property is set, {@code false} otherwise
423 */
424 public static Boolean is_prop_set(final Environment env, String key, String layer) {
425 return env.getCascade(layer).containsKey(key);
426 }
427
428 /**
429 * Gets the value of the key {@code key} from the object in question.
430 * @param env the environment
431 * @param key the OSM key
432 * @return the value for given key
433 */
434 public static String tag(final Environment env, String key) {
435 return env.osm == null ? null : env.osm.get(key);
436 }
437
438 /**
439 * Gets the first non-null value of the key {@code key} from the object's parent(s).
440 * @param env the environment
441 * @param key the OSM key
442 * @return first non-null value of the key {@code key} from the object's parent(s)
443 */
444 public static String parent_tag(final Environment env, String key) {
445 if (env.parent == null) {
446 if (env.osm != null) {
447 // we don't have a matched parent, so just search all referrers
448 for (OsmPrimitive parent : env.osm.getReferrers()) {
449 String value = parent.get(key);
450 if (value != null) {
451 return value;
452 }
453 }
454 }
455 return null;
456 }
457 return env.parent.get(key);
458 }
459
460 /**
461 * Gets the value of the key {@code key} from the object's child.
462 * @param env the environment
463 * @param key the OSM key
464 * @return the value of the key {@code key} from the object's child, or {@code null} if there is no child
465 */
466 public static String child_tag(final Environment env, String key) {
467 return env.child == null ? null : env.child.get(key);
468 }
469
470 /**
471 * Determines whether the object has a tag with the given key.
472 * @param env the environment
473 * @param key the OSM key
474 * @return {@code true} if the object has a tag with the given key, {@code false} otherwise
475 */
476 public static boolean has_tag_key(final Environment env, String key) {
477 return env.osm.hasKey(key);
478 }
479
480 /**
481 * Returns the index of node in parent way or member in parent relation.
482 * @param env the environment
483 * @return the index as float. Starts at 1
484 */
485 public static Float index(final Environment env) {
486 if (env.index == null) {
487 return null;
488 }
489 return new Float(env.index + 1);
490 }
491
492 /**
493 * Returns the role of current object in parent relation, or role of child if current object is a relation.
494 * @param env the environment
495 * @return role of current object in parent relation, or role of child if current object is a relation
496 * @see Environment#getRole()
497 */
498 public static String role(final Environment env) {
499 return env.getRole();
500 }
501
502 /**
503 * Returns the area of a closed way in square meters or {@code null}.
504 * @param env the environment
505 * @return the area of a closed way in square meters or {@code null}
506 * @see Geometry#closedWayArea(Way)
507 */
508 public static Float areasize(final Environment env) {
509 if (env.osm instanceof Way && ((Way) env.osm).isClosed()) {
510 return (float) Geometry.closedWayArea((Way) env.osm);
511 } else {
512 return null;
513 }
514 }
515
516 /**
517 * Returns the length of the way in metres or {@code null}.
518 * @param env the environment
519 * @return the length of the way in metres or {@code null}.
520 * @see Way#getLength()
521 */
522 public static Float waylength(final Environment env) {
523 if (env.osm instanceof Way) {
524 return (float) ((Way) env.osm).getLength();
525 } else {
526 return null;
527 }
528 }
529
530 /**
531 * Function associated to the logical "!" operator.
532 * @param b boolean value
533 * @return {@code true} if {@code !b}
534 */
535 public static boolean not(boolean b) {
536 return !b;
537 }
538
539 /**
540 * Function associated to the logical ">=" operator.
541 * @param a first value
542 * @param b second value
543 * @return {@code true} if {@code a >= b}
544 */
545 public static boolean greater_equal(float a, float b) {
546 return a >= b;
547 }
548
549 /**
550 * Function associated to the logical "<=" operator.
551 * @param a first value
552 * @param b second value
553 * @return {@code true} if {@code a <= b}
554 */
555 public static boolean less_equal(float a, float b) {
556 return a <= b;
557 }
558
559 /**
560 * Function associated to the logical ">" operator.
561 * @param a first value
562 * @param b second value
563 * @return {@code true} if {@code a > b}
564 */
565 public static boolean greater(float a, float b) {
566 return a > b;
567 }
568
569 /**
570 * Function associated to the logical "<" operator.
571 * @param a first value
572 * @param b second value
573 * @return {@code true} if {@code a < b}
574 */
575 public static boolean less(float a, float b) {
576 return a < b;
577 }
578
579 /**
580 * Converts an angle in degrees to radians.
581 * @param degree the angle in degrees
582 * @return the angle in radians
583 * @see Math#toRadians(double)
584 */
585 public static double degree_to_radians(double degree) {
586 return Math.toRadians(degree);
587 }
588
589 /**
590 * Converts an angle diven in cardinal directions to radians.
591 * The following values are supported: {@code n}, {@code north}, {@code ne}, {@code northeast},
592 * {@code e}, {@code east}, {@code se}, {@code southeast}, {@code s}, {@code south},
593 * {@code sw}, {@code southwest}, {@code w}, {@code west}, {@code nw}, {@code northwest}.
594 * @param cardinal the angle in cardinal directions.
595 * @see RotationAngle#parseCardinalRotation(String)
596 * @return the angle in radians
597 */
598 public static Double cardinal_to_radians(String cardinal) {
599 try {
600 return RotationAngle.parseCardinalRotation(cardinal);
601 } catch (IllegalArgumentException ignore) {
602 return null;
603 }
604 }
605
606 /**
607 * Determines if the objects {@code a} and {@code b} are equal.
608 * @param a First object
609 * @param b Second object
610 * @return {@code true} if objects are equal, {@code false} otherwise
611 * @see Object#equals(Object)
612 */
613 public static boolean equal(Object a, Object b) {
614 if (a.getClass() == b.getClass()) return a.equals(b);
615 if (a.equals(Cascade.convertTo(b, a.getClass()))) return true;
616 return b.equals(Cascade.convertTo(a, b.getClass()));
617 }
618
619 /**
620 * Determines if the objects {@code a} and {@code b} are not equal.
621 * @param a First object
622 * @param b Second object
623 * @return {@code false} if objects are equal, {@code true} otherwise
624 * @see Object#equals(Object)
625 */
626 public static boolean not_equal(Object a, Object b) {
627 return !equal(a,b);
628 }
629 /**
630 * Determines whether the JOSM search with {@code searchStr} applies to the object.
631 * @param env the environment
632 * @param searchStr the search string
633 * @return {@code true} if the JOSM search with {@code searchStr} applies to the object
634 * @see SearchCompiler
635 */
636 public static Boolean JOSM_search(final Environment env, String searchStr) {
637 Match m;
638 try {
639 m = SearchCompiler.compile(searchStr, false, false);
640 } catch (ParseError ex) {
641 return null;
642 }
643 return m.match(env.osm);
644 }
645
646 /**
647 * Obtains the JOSM'key {@link org.openstreetmap.josm.data.Preferences} string for key {@code key},
648 * and defaults to {@code def} if that is null.
649 * @param key Key in JOSM preference
650 * @param def Default value
651 * @return value for key, or default value if not found
652 * @see org.openstreetmap.josm.data.Preferences#get(String, String)
653 */
654 public static String JOSM_pref(String key, String def) {
655 String res = Main.pref.get(key, null);
656 return res != null ? res : def;
657 }
658
659 /**
660 * Tests if string {@code target} matches pattern {@code pattern}
661 * @param pattern The regex expression
662 * @param target The character sequence to be matched
663 * @return {@code true} if, and only if, the entire region sequence matches the pattern
664 * @see Pattern#matches(String, CharSequence)
665 * @since 5699
666 */
667 public static boolean regexp_test(String pattern, String target) {
668 return Pattern.matches(pattern, target);
669 }
670
671 /**
672 * Tests if string {@code target} matches pattern {@code pattern}
673 * @param pattern The regex expression
674 * @param target The character sequence to be matched
675 * @param flags a string that may contain "i" (case insensitive), "m" (multiline) and "s" ("dot all")
676 * @return {@code true} if, and only if, the entire region sequence matches the pattern
677 * @see Pattern#CASE_INSENSITIVE
678 * @see Pattern#DOTALL
679 * @see Pattern#MULTILINE
680 * @since 5699
681 */
682 public static boolean regexp_test(String pattern, String target, String flags) {
683 int f = 0;
684 if (flags.contains("i")) {
685 f |= Pattern.CASE_INSENSITIVE;
686 }
687 if (flags.contains("s")) {
688 f |= Pattern.DOTALL;
689 }
690 if (flags.contains("m")) {
691 f |= Pattern.MULTILINE;
692 }
693 return Pattern.compile(pattern, f).matcher(target).matches();
694 }
695
696 /**
697 * Tries to match string against pattern regexp and returns a list of capture groups in case of success.
698 * The first element (index 0) is the complete match (i.e. string).
699 * Further elements correspond to the bracketed parts of the regular expression.
700 * @param pattern The regex expression
701 * @param target The character sequence to be matched
702 * @param flags a string that may contain "i" (case insensitive), "m" (multiline) and "s" ("dot all")
703 * @return a list of capture groups if {@link Matcher#matches()}, or {@code null}.
704 * @see Pattern#CASE_INSENSITIVE
705 * @see Pattern#DOTALL
706 * @see Pattern#MULTILINE
707 * @since 5701
708 */
709 public static List<String> regexp_match(String pattern, String target, String flags) {
710 int f = 0;
711 if (flags.contains("i")) {
712 f |= Pattern.CASE_INSENSITIVE;
713 }
714 if (flags.contains("s")) {
715 f |= Pattern.DOTALL;
716 }
717 if (flags.contains("m")) {
718 f |= Pattern.MULTILINE;
719 }
720 return Utils.getMatches(Pattern.compile(pattern, f).matcher(target));
721 }
722
723 /**
724 * Tries to match string against pattern regexp and returns a list of capture groups in case of success.
725 * The first element (index 0) is the complete match (i.e. string).
726 * Further elements correspond to the bracketed parts of the regular expression.
727 * @param pattern The regex expression
728 * @param target The character sequence to be matched
729 * @return a list of capture groups if {@link Matcher#matches()}, or {@code null}.
730 * @since 5701
731 */
732 public static List<String> regexp_match(String pattern, String target) {
733 return Utils.getMatches(Pattern.compile(pattern).matcher(target));
734 }
735
736 /**
737 * Returns the OSM id of the current object.
738 * @param env the environment
739 * @return the OSM id of the current object
740 * @see OsmPrimitive#getUniqueId()
741 */
742 public static long osm_id(final Environment env) {
743 return env.osm.getUniqueId();
744 }
745
746 /**
747 * Translates some text for the current locale. The first argument is the text to translate,
748 * and the subsequent arguments are parameters for the string indicated by <code>{0}</code>, <code>{1}</code>, …
749 * @param args arguments
750 * @return the translated string
751 */
752 @NullableArguments
753 public static String tr(String... args) {
754 final String text = args[0];
755 System.arraycopy(args, 1, args, 0, args.length - 1);
756 return org.openstreetmap.josm.tools.I18n.tr(text, (Object[])args);
757 }
758
759 /**
760 * Returns the substring of {@code s} starting at index {@code begin} (inclusive, 0-indexed).
761 * @param s The base string
762 * @param begin The start index
763 * @return the substring
764 * @see String#substring(int)
765 */
766 public static String substring(String s, /* due to missing Cascade.convertTo for int*/ float begin) {
767 return s == null ? null : s.substring((int) begin);
768 }
769
770 /**
771 * Returns the substring of {@code s} starting at index {@code begin} (inclusive)
772 * and ending at index {@code end}, (exclusive, 0-indexed).
773 * @param s The base string
774 * @param begin The start index
775 * @param end The end index
776 * @return the substring
777 * @see String#substring(int, int)
778 */
779 public static String substring(String s, float begin, float end) {
780 return s == null ? null : s.substring((int) begin, (int) end);
781 }
782
783 /**
784 * Replaces in {@code s} every {@code} target} substring by {@code replacement}.
785 * @param s The source string
786 * @param target The sequence of char values to be replaced
787 * @param replacement The replacement sequence of char values
788 * @return The resulting string
789 * @see String#replace(CharSequence, CharSequence)
790 */
791 public static String replace(String s, String target, String replacement) {
792 return s == null ? null : s.replace(target, replacement);
793 }
794
795 /**
796 * Percent-encode a string. (See https://en.wikipedia.org/wiki/Percent-encoding)
797 * This is especially useful for data urls, e.g.
798 * <code>concat("data:image/svg+xml,", URL_encode("&lt;svg&gt;...&lt;/svg&gt;"));</code>
799 * @param s arbitrary string
800 * @return the encoded string
801 */
802 public static String URL_encode(String s) {
803 try {
804 return s == null ? null : URLEncoder.encode(s, "UTF-8");
805 } catch (UnsupportedEncodingException ex) {
806 throw new RuntimeException(ex);
807 }
808 }
809
810 /**
811 * XML-encode a string.
812 *
813 * Escapes special characters in xml. Alternative to using &lt;![CDATA[ ... ]]&gt; blocks.
814 * @param s arbitrary string
815 * @return the encoded string
816 */
817 public static String XML_encode(String s) {
818 return s == null ? null : XmlWriter.encode(s);
819 }
820
821 /**
822 * Calculates the CRC32 checksum from a string (based on RFC 1952).
823 * @param s the string
824 * @return long value from 0 to 2^32-1
825 */
826 public static long CRC32_checksum(String s) {
827 CRC32 cs = new CRC32();
828 cs.update(s.getBytes(StandardCharsets.UTF_8));
829 return cs.getValue();
830 }
831
832 /**
833 * check if there is right-hand traffic at the current location
834 * @param env the environment
835 * @return true if there is right-hand traffic
836 * @since 7193
837 */
838 public static boolean is_right_hand_traffic(Environment env) {
839 if (env.osm instanceof Node)
840 return RightAndLefthandTraffic.isRightHandTraffic(((Node) env.osm).getCoor());
841 return RightAndLefthandTraffic.isRightHandTraffic(env.osm.getBBox().getCenter());
842 }
843
844 /**
845 * Prints the object to the command line (for debugging purpose).
846 * @param o the object
847 * @return the same object, unchanged
848 */
849 @NullableArguments
850 public static Object print(Object o) {
851 System.out.print(o == null ? "none" : o.toString());
852 return o;
853 }
854
855 /**
856 * Prints the object to the command line, with new line at the end
857 * (for debugging purpose).
858 * @param o the object
859 * @return the same object, unchanged
860 */
861 @NullableArguments
862 public static Object println(Object o) {
863 System.out.println(o == null ? "none" : o.toString());
864 return o;
865 }
866
867 /**
868 * Get the number of tags for the current primitive.
869 * @param env the environment
870 * @return number of tags
871 */
872 public static int number_of_tags(Environment env) {
873 return env.osm.getNumKeys();
874 }
875
876 /**
877 * Get value of a setting.
878 * @param env the environment
879 * @param key setting key (given as layer identifier, e.g. setting::mykey {...})
880 * @return the value of the setting (calculated when the style is loaded)
881 */
882 public static Object setting(Environment env, String key) {
883 return env.source.settingValues.get(key);
884 }
885 }
886
887 /**
888 * Main method to create an function-like expression.
889 *
890 * @param name the name of the function or operator
891 * @param args the list of arguments (as expressions)
892 * @return the generated Expression. If no suitable function can be found,
893 * returns {@link NullExpression#INSTANCE}.
894 */
895 public static Expression createFunctionExpression(String name, List<Expression> args) {
896 if ("cond".equals(name) && args.size() == 3)
897 return new CondOperator(args.get(0), args.get(1), args.get(2));
898 else if ("and".equals(name))
899 return new AndOperator(args);
900 else if ("or".equals(name))
901 return new OrOperator(args);
902 else if ("length".equals(name) && args.size() == 1)
903 return new LengthFunction(args.get(0));
904 else if ("max".equals(name) && !args.isEmpty())
905 return new MinMaxFunction(args, true);
906 else if ("min".equals(name) && !args.isEmpty())
907 return new MinMaxFunction(args, false);
908
909 for (Method m : arrayFunctions) {
910 if (m.getName().equals(name))
911 return new ArrayFunction(m, args);
912 }
913 for (Method m : parameterFunctions) {
914 if (m.getName().equals(name) && args.size() == m.getParameterTypes().length)
915 return new ParameterFunction(m, args, false);
916 }
917 for (Method m : parameterFunctionsEnv) {
918 if (m.getName().equals(name) && args.size() == m.getParameterTypes().length-1)
919 return new ParameterFunction(m, args, true);
920 }
921 return NullExpression.INSTANCE;
922 }
923
924 /**
925 * Expression that always evaluates to null.
926 */
927 public static class NullExpression implements Expression {
928
929 /**
930 * The unique instance.
931 */
932 public static final NullExpression INSTANCE = new NullExpression();
933
934 @Override
935 public Object evaluate(Environment env) {
936 return null;
937 }
938 }
939
940 /**
941 * Conditional operator.
942 */
943 public static class CondOperator implements Expression {
944
945 private Expression condition, firstOption, secondOption;
946
947 /**
948 * Constructs a new {@code CondOperator}.
949 * @param condition condition
950 * @param firstOption first option
951 * @param secondOption second option
952 */
953 public CondOperator(Expression condition, Expression firstOption, Expression secondOption) {
954 this.condition = condition;
955 this.firstOption = firstOption;
956 this.secondOption = secondOption;
957 }
958
959 @Override
960 public Object evaluate(Environment env) {
961 Boolean b = Cascade.convertTo(condition.evaluate(env), boolean.class);
962 if (b != null && b)
963 return firstOption.evaluate(env);
964 else
965 return secondOption.evaluate(env);
966 }
967 }
968
969 /**
970 * "And" logical operator.
971 */
972 public static class AndOperator implements Expression {
973
974 private List<Expression> args;
975
976 /**
977 * Constructs a new {@code AndOperator}.
978 * @param args arguments
979 */
980 public AndOperator(List<Expression> args) {
981 this.args = args;
982 }
983
984 @Override
985 public Object evaluate(Environment env) {
986 for (Expression arg : args) {
987 Boolean b = Cascade.convertTo(arg.evaluate(env), boolean.class);
988 if (b == null || !b) {
989 return false;
990 }
991 }
992 return true;
993 }
994 }
995
996 /**
997 * "Or" logical operator.
998 */
999 public static class OrOperator implements Expression {
1000
1001 private List<Expression> args;
1002
1003 /**
1004 * Constructs a new {@code OrOperator}.
1005 * @param args arguments
1006 */
1007 public OrOperator(List<Expression> args) {
1008 this.args = args;
1009 }
1010
1011 @Override
1012 public Object evaluate(Environment env) {
1013 for (Expression arg : args) {
1014 Boolean b = Cascade.convertTo(arg.evaluate(env), boolean.class);
1015 if (b != null && b) {
1016 return true;
1017 }
1018 }
1019 return false;
1020 }
1021 }
1022
1023 /**
1024 * Function to calculate the length of a string or list in a MapCSS eval expression.
1025 *
1026 * Separate implementation to support overloading for different argument types.
1027 *
1028 * The use for calculating the length of a list is deprecated, use
1029 * {@link Functions#count(java.util.List)} instead (see #10061).
1030 */
1031 public static class LengthFunction implements Expression {
1032
1033 private Expression arg;
1034
1035 /**
1036 * Constructs a new {@code LengthFunction}.
1037 * @param args arguments
1038 */
1039 public LengthFunction(Expression args) {
1040 this.arg = args;
1041 }
1042
1043 @Override
1044 public Object evaluate(Environment env) {
1045 List<?> l = Cascade.convertTo(arg.evaluate(env), List.class);
1046 if (l != null)
1047 return l.size();
1048 String s = Cascade.convertTo(arg.evaluate(env), String.class);
1049 if (s != null)
1050 return s.length();
1051 return null;
1052 }
1053 }
1054
1055 /**
1056 * Computes the maximum/minimum value an arbitrary number of floats, or a list of floats.
1057 */
1058 public static class MinMaxFunction implements Expression {
1059
1060 private final List<Expression> args;
1061 private final boolean computeMax;
1062
1063 /**
1064 * Constructs a new {@code MinMaxFunction}.
1065 * @param args arguments
1066 * @param computeMax if {@code true}, compute max. If {@code false}, compute min
1067 */
1068 public MinMaxFunction(final List<Expression> args, final boolean computeMax) {
1069 this.args = args;
1070 this.computeMax = computeMax;
1071 }
1072
1073 public Float aggregateList(List<?> lst) {
1074 final List<Float> floats = Utils.transform(lst, new Utils.Function<Object, Float>() {
1075 @Override
1076 public Float apply(Object x) {
1077 return Cascade.convertTo(x, float.class);
1078 }
1079 });
1080 final Collection<Float> nonNullList = Utils.filter(floats, Predicates.not(Predicates.isNull()));
1081 return computeMax ? Collections.max(nonNullList) : Collections.min(nonNullList);
1082 }
1083
1084 @Override
1085 public Object evaluate(final Environment env) {
1086 List<?> l = Cascade.convertTo(args.get(0).evaluate(env), List.class);
1087 if (args.size() != 1 || l == null)
1088 l = Utils.transform(args, new Utils.Function<Expression, Object>() {
1089 @Override
1090 public Object apply(Expression x) {
1091 return x.evaluate(env);
1092 }
1093 });
1094 return aggregateList(l);
1095 }
1096 }
1097
1098 /**
1099 * Function that takes a certain number of argument with specific type.
1100 *
1101 * Implementation is based on a Method object.
1102 * If any of the arguments evaluate to null, the result will also be null.
1103 */
1104 public static class ParameterFunction implements Expression {
1105
1106 private final Method m;
1107 private final boolean nullable;
1108 private final List<Expression> args;
1109 private final Class<?>[] expectedParameterTypes;
1110 private final boolean needsEnvironment;
1111
1112 /**
1113 * Constructs a new {@code ParameterFunction}.
1114 */
1115 public ParameterFunction(Method m, List<Expression> args, boolean needsEnvironment) {
1116 this.m = m;
1117 this.nullable = m.getAnnotation(NullableArguments.class) != null;
1118 this.args = args;
1119 this.expectedParameterTypes = m.getParameterTypes();
1120 this.needsEnvironment = needsEnvironment;
1121 }
1122
1123 @Override
1124 public Object evaluate(Environment env) {
1125 Object[] convertedArgs;
1126
1127 if (needsEnvironment) {
1128 convertedArgs = new Object[args.size()+1];
1129 convertedArgs[0] = env;
1130 for (int i = 1; i < convertedArgs.length; ++i) {
1131 convertedArgs[i] = Cascade.convertTo(args.get(i-1).evaluate(env), expectedParameterTypes[i]);
1132 if (convertedArgs[i] == null && !nullable) {
1133 return null;
1134 }
1135 }
1136 } else {
1137 convertedArgs = new Object[args.size()];
1138 for (int i = 0; i < convertedArgs.length; ++i) {
1139 convertedArgs[i] = Cascade.convertTo(args.get(i).evaluate(env), expectedParameterTypes[i]);
1140 if (convertedArgs[i] == null && !nullable) {
1141 return null;
1142 }
1143 }
1144 }
1145 Object result = null;
1146 try {
1147 result = m.invoke(null, convertedArgs);
1148 } catch (IllegalAccessException | IllegalArgumentException ex) {
1149 throw new RuntimeException(ex);
1150 } catch (InvocationTargetException ex) {
1151 Main.error(ex);
1152 return null;
1153 }
1154 return result;
1155 }
1156
1157 @Override
1158 public String toString() {
1159 StringBuilder b = new StringBuilder("ParameterFunction~");
1160 b.append(m.getName()).append("(");
1161 for (int i = 0; i < args.size(); ++i) {
1162 if (i > 0) b.append(",");
1163 b.append(expectedParameterTypes[i]);
1164 b.append(" ").append(args.get(i));
1165 }
1166 b.append(')');
1167 return b.toString();
1168 }
1169 }
1170
1171 /**
1172 * Function that takes an arbitrary number of arguments.
1173 *
1174 * Currently, all array functions are static, so there is no need to
1175 * provide the environment, like it is done in {@link ParameterFunction}.
1176 * If any of the arguments evaluate to null, the result will also be null.
1177 */
1178 public static class ArrayFunction implements Expression {
1179
1180 private final Method m;
1181 private final boolean nullable;
1182 private final List<Expression> args;
1183 private final Class<?>[] expectedParameterTypes;
1184 private final Class<?> arrayComponentType;
1185
1186 /**
1187 * Constructs a new {@code ArrayFunction}.
1188 */
1189 public ArrayFunction(Method m, List<Expression> args) {
1190 this.m = m;
1191 this.nullable = m.getAnnotation(NullableArguments.class) != null;
1192 this.args = args;
1193 this.expectedParameterTypes = m.getParameterTypes();
1194 this.arrayComponentType = expectedParameterTypes[0].getComponentType();
1195 }
1196
1197 @Override
1198 public Object evaluate(Environment env) {
1199 Object[] convertedArgs = new Object[expectedParameterTypes.length];
1200 Object arrayArg = Array.newInstance(arrayComponentType, args.size());
1201 for (int i = 0; i < args.size(); ++i) {
1202 Object o = Cascade.convertTo(args.get(i).evaluate(env), arrayComponentType);
1203 if (o == null && !nullable) {
1204 return null;
1205 }
1206 Array.set(arrayArg, i, o);
1207 }
1208 convertedArgs[0] = arrayArg;
1209
1210 Object result = null;
1211 try {
1212 result = m.invoke(null, convertedArgs);
1213 } catch (IllegalAccessException | IllegalArgumentException ex) {
1214 throw new RuntimeException(ex);
1215 } catch (InvocationTargetException ex) {
1216 Main.error(ex);
1217 return null;
1218 }
1219 return result;
1220 }
1221
1222 @Override
1223 public String toString() {
1224 StringBuilder b = new StringBuilder("ArrayFunction~");
1225 b.append(m.getName()).append("(");
1226 for (int i = 0; i < args.size(); ++i) {
1227 if (i > 0) b.append(",");
1228 b.append(arrayComponentType);
1229 b.append(" ").append(args.get(i));
1230 }
1231 b.append(')');
1232 return b.toString();
1233 }
1234 }
1235}
Note: See TracBrowser for help on using the repository browser.