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