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