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

Last change on this file since 7136 was 7136, checked in by bastiK, 10 years ago

mapcss: add support for alpha info in color property, e.g.
color: #aa0022ee; or color: rgba(1.0, 0.2, 0.8, 0.8);
*opacity properties still have higher priority, if specified
explicitly

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