001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.mappaint.mapcss; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Color; 007import java.io.ByteArrayInputStream; 008import java.io.File; 009import java.io.IOException; 010import java.io.InputStream; 011import java.lang.reflect.Field; 012import java.nio.charset.StandardCharsets; 013import java.text.MessageFormat; 014import java.util.ArrayList; 015import java.util.BitSet; 016import java.util.Collections; 017import java.util.HashMap; 018import java.util.HashSet; 019import java.util.Iterator; 020import java.util.List; 021import java.util.Locale; 022import java.util.Map; 023import java.util.Map.Entry; 024import java.util.NoSuchElementException; 025import java.util.Set; 026import java.util.concurrent.locks.ReadWriteLock; 027import java.util.concurrent.locks.ReentrantReadWriteLock; 028import java.util.zip.ZipEntry; 029import java.util.zip.ZipFile; 030 031import org.openstreetmap.josm.data.Version; 032import org.openstreetmap.josm.data.osm.INode; 033import org.openstreetmap.josm.data.osm.IPrimitive; 034import org.openstreetmap.josm.data.osm.IRelation; 035import org.openstreetmap.josm.data.osm.IWay; 036import org.openstreetmap.josm.data.osm.KeyValueVisitor; 037import org.openstreetmap.josm.data.osm.Node; 038import org.openstreetmap.josm.data.osm.OsmUtils; 039import org.openstreetmap.josm.data.osm.Tagged; 040import org.openstreetmap.josm.data.preferences.sources.SourceEntry; 041import org.openstreetmap.josm.gui.mappaint.Cascade; 042import org.openstreetmap.josm.gui.mappaint.Environment; 043import org.openstreetmap.josm.gui.mappaint.MultiCascade; 044import org.openstreetmap.josm.gui.mappaint.Range; 045import org.openstreetmap.josm.gui.mappaint.StyleKeys; 046import org.openstreetmap.josm.gui.mappaint.StyleSetting; 047import org.openstreetmap.josm.gui.mappaint.StyleSetting.BooleanStyleSetting; 048import org.openstreetmap.josm.gui.mappaint.StyleSource; 049import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.KeyCondition; 050import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.KeyMatchType; 051import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.KeyValueCondition; 052import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.Op; 053import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.PseudoClassCondition; 054import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.SimpleKeyValueCondition; 055import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.AbstractSelector; 056import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.ChildOrParentSelector; 057import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector; 058import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.OptimizedGeneralSelector; 059import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser; 060import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException; 061import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.TokenMgrError; 062import org.openstreetmap.josm.gui.mappaint.styleelement.LineElement; 063import org.openstreetmap.josm.io.CachedFile; 064import org.openstreetmap.josm.tools.CheckParameterUtil; 065import org.openstreetmap.josm.tools.I18n; 066import org.openstreetmap.josm.tools.JosmRuntimeException; 067import org.openstreetmap.josm.tools.LanguageInfo; 068import org.openstreetmap.josm.tools.Logging; 069import org.openstreetmap.josm.tools.Utils; 070 071/** 072 * This is a mappaint style that is based on MapCSS rules. 073 */ 074public class MapCSSStyleSource extends StyleSource { 075 076 /** 077 * The accepted MIME types sent in the HTTP Accept header. 078 * @since 6867 079 */ 080 public static final String MAPCSS_STYLE_MIME_TYPES = 081 "text/x-mapcss, text/mapcss, text/css; q=0.9, text/plain; q=0.8, application/zip, application/octet-stream; q=0.5"; 082 083 /** 084 * all rules in this style file 085 */ 086 public final List<MapCSSRule> rules = new ArrayList<>(); 087 /** 088 * Rules for nodes 089 */ 090 public final MapCSSRuleIndex nodeRules = new MapCSSRuleIndex(); 091 /** 092 * Rules for ways without tag area=no 093 */ 094 public final MapCSSRuleIndex wayRules = new MapCSSRuleIndex(); 095 /** 096 * Rules for ways with tag area=no 097 */ 098 public final MapCSSRuleIndex wayNoAreaRules = new MapCSSRuleIndex(); 099 /** 100 * Rules for relations that are not multipolygon relations 101 */ 102 public final MapCSSRuleIndex relationRules = new MapCSSRuleIndex(); 103 /** 104 * Rules for multipolygon relations 105 */ 106 public final MapCSSRuleIndex multipolygonRules = new MapCSSRuleIndex(); 107 /** 108 * rules to apply canvas properties 109 */ 110 public final MapCSSRuleIndex canvasRules = new MapCSSRuleIndex(); 111 112 private Color backgroundColorOverride; 113 private String css; 114 private ZipFile zipFile; 115 116 /** 117 * This lock prevents concurrent execution of {@link MapCSSRuleIndex#clear() } / 118 * {@link MapCSSRuleIndex#initIndex()} and {@link MapCSSRuleIndex#getRuleCandidates }. 119 * 120 * For efficiency reasons, these methods are synchronized higher up the 121 * stack trace. 122 */ 123 public static final ReadWriteLock STYLE_SOURCE_LOCK = new ReentrantReadWriteLock(); 124 125 /** 126 * Set of all supported MapCSS keys. 127 */ 128 static final Set<String> SUPPORTED_KEYS = new HashSet<>(); 129 static { 130 Field[] declaredFields = StyleKeys.class.getDeclaredFields(); 131 for (Field f : declaredFields) { 132 try { 133 SUPPORTED_KEYS.add((String) f.get(null)); 134 if (!f.getName().toLowerCase(Locale.ENGLISH).replace('_', '-').equals(f.get(null))) { 135 throw new JosmRuntimeException(f.getName()); 136 } 137 } catch (IllegalArgumentException | IllegalAccessException ex) { 138 throw new JosmRuntimeException(ex); 139 } 140 } 141 for (LineElement.LineType lt : LineElement.LineType.values()) { 142 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.COLOR); 143 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES); 144 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES_BACKGROUND_COLOR); 145 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES_BACKGROUND_OPACITY); 146 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES_OFFSET); 147 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.LINECAP); 148 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.LINEJOIN); 149 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.MITERLIMIT); 150 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.OFFSET); 151 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.OPACITY); 152 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.REAL_WIDTH); 153 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.WIDTH); 154 } 155 } 156 157 /** 158 * A collection of {@link MapCSSRule}s, that are indexed by tag key and value. 159 * 160 * Speeds up the process of finding all rules that match a certain primitive. 161 * 162 * Rules with a {@link SimpleKeyValueCondition} [key=value] or rules that require a specific key to be set are 163 * indexed. Now you only need to loop the tags of a primitive to retrieve the possibly matching rules. 164 * 165 * To use this index, you need to {@link #add(MapCSSRule)} all rules to it. You then need to call 166 * {@link #initIndex()}. Afterwards, you can use {@link #getRuleCandidates(IPrimitive)} to get an iterator over 167 * all rules that might be applied to that primitive. 168 */ 169 public static class MapCSSRuleIndex { 170 /** 171 * This is an iterator over all rules that are marked as possible in the bitset. 172 * 173 * @author Michael Zangl 174 */ 175 private final class RuleCandidatesIterator implements Iterator<MapCSSRule>, KeyValueVisitor { 176 private final BitSet ruleCandidates; 177 private int next; 178 179 private RuleCandidatesIterator(BitSet ruleCandidates) { 180 this.ruleCandidates = ruleCandidates; 181 } 182 183 @Override 184 public boolean hasNext() { 185 return next >= 0 && next < rules.size(); 186 } 187 188 @Override 189 public MapCSSRule next() { 190 if (!hasNext()) 191 throw new NoSuchElementException(); 192 MapCSSRule rule = rules.get(next); 193 next = ruleCandidates.nextSetBit(next + 1); 194 return rule; 195 } 196 197 @Override 198 public void remove() { 199 throw new UnsupportedOperationException(); 200 } 201 202 @Override 203 public void visitKeyValue(Tagged p, String key, String value) { 204 MapCSSKeyRules v = index.get(key); 205 if (v != null) { 206 BitSet rs = v.get(value); 207 ruleCandidates.or(rs); 208 } 209 } 210 211 /** 212 * Call this before using the iterator. 213 */ 214 public void prepare() { 215 next = ruleCandidates.nextSetBit(0); 216 } 217 } 218 219 /** 220 * This is a map of all rules that are only applied if the primitive has a given key (and possibly value) 221 * 222 * @author Michael Zangl 223 */ 224 private static final class MapCSSKeyRules { 225 /** 226 * The indexes of rules that might be applied if this tag is present and the value has no special handling. 227 */ 228 BitSet generalRules = new BitSet(); 229 230 /** 231 * A map that sores the indexes of rules that might be applied if the key=value pair is present on this 232 * primitive. This includes all key=* rules. 233 */ 234 Map<String, BitSet> specialRules = new HashMap<>(); 235 236 public void addForKey(int ruleIndex) { 237 generalRules.set(ruleIndex); 238 for (BitSet r : specialRules.values()) { 239 r.set(ruleIndex); 240 } 241 } 242 243 public void addForKeyAndValue(String value, int ruleIndex) { 244 BitSet forValue = specialRules.get(value); 245 if (forValue == null) { 246 forValue = new BitSet(); 247 forValue.or(generalRules); 248 specialRules.put(value.intern(), forValue); 249 } 250 forValue.set(ruleIndex); 251 } 252 253 public BitSet get(String value) { 254 BitSet forValue = specialRules.get(value); 255 if (forValue != null) return forValue; else return generalRules; 256 } 257 } 258 259 /** 260 * All rules this index is for. Once this index is built, this list is sorted. 261 */ 262 private final List<MapCSSRule> rules = new ArrayList<>(); 263 /** 264 * All rules that only apply when the given key is present. 265 */ 266 private final Map<String, MapCSSKeyRules> index = new HashMap<>(); 267 /** 268 * Rules that do not require any key to be present. Only the index in the {@link #rules} array is stored. 269 */ 270 private final BitSet remaining = new BitSet(); 271 272 /** 273 * Add a rule to this index. This needs to be called before {@link #initIndex()} is called. 274 * @param rule The rule to add. 275 */ 276 public void add(MapCSSRule rule) { 277 rules.add(rule); 278 } 279 280 /** 281 * Initialize the index. 282 * <p> 283 * You must own the write lock of STYLE_SOURCE_LOCK when calling this method. 284 */ 285 public void initIndex() { 286 Collections.sort(rules); 287 for (int ruleIndex = 0; ruleIndex < rules.size(); ruleIndex++) { 288 MapCSSRule r = rules.get(ruleIndex); 289 // find the rightmost selector, this must be a GeneralSelector 290 Selector selRightmost = r.selector; 291 while (selRightmost instanceof ChildOrParentSelector) { 292 selRightmost = ((ChildOrParentSelector) selRightmost).right; 293 } 294 OptimizedGeneralSelector s = (OptimizedGeneralSelector) selRightmost; 295 if (s.conds == null) { 296 remaining.set(ruleIndex); 297 continue; 298 } 299 List<SimpleKeyValueCondition> sk = new ArrayList<>(Utils.filteredCollection(s.conds, 300 SimpleKeyValueCondition.class)); 301 if (!sk.isEmpty()) { 302 SimpleKeyValueCondition c = sk.get(sk.size() - 1); 303 getEntryInIndex(c.k).addForKeyAndValue(c.v, ruleIndex); 304 } else { 305 String key = findAnyRequiredKey(s.conds); 306 if (key != null) { 307 getEntryInIndex(key).addForKey(ruleIndex); 308 } else { 309 remaining.set(ruleIndex); 310 } 311 } 312 } 313 } 314 315 /** 316 * Search for any key that condition might depend on. 317 * 318 * @param conds The conditions to search through. 319 * @return An arbitrary key this rule depends on or <code>null</code> if there is no such key. 320 */ 321 private static String findAnyRequiredKey(List<Condition> conds) { 322 String key = null; 323 for (Condition c : conds) { 324 if (c instanceof KeyCondition) { 325 KeyCondition keyCondition = (KeyCondition) c; 326 if (!keyCondition.negateResult && conditionRequiresKeyPresence(keyCondition.matchType)) { 327 key = keyCondition.label; 328 } 329 } else if (c instanceof KeyValueCondition) { 330 KeyValueCondition keyValueCondition = (KeyValueCondition) c; 331 if (!Op.NEGATED_OPS.contains(keyValueCondition.op)) { 332 key = keyValueCondition.k; 333 } 334 } 335 } 336 return key; 337 } 338 339 private static boolean conditionRequiresKeyPresence(KeyMatchType matchType) { 340 return matchType != KeyMatchType.REGEX; 341 } 342 343 private MapCSSKeyRules getEntryInIndex(String key) { 344 MapCSSKeyRules rulesWithMatchingKey = index.get(key); 345 if (rulesWithMatchingKey == null) { 346 rulesWithMatchingKey = new MapCSSKeyRules(); 347 index.put(key.intern(), rulesWithMatchingKey); 348 } 349 return rulesWithMatchingKey; 350 } 351 352 /** 353 * Get a subset of all rules that might match the primitive. Rules not included in the result are guaranteed to 354 * not match this primitive. 355 * <p> 356 * You must have a read lock of STYLE_SOURCE_LOCK when calling this method. 357 * 358 * @param osm the primitive to match 359 * @return An iterator over possible rules in the right order. 360 * @since 13810 (signature) 361 */ 362 public Iterator<MapCSSRule> getRuleCandidates(IPrimitive osm) { 363 final BitSet ruleCandidates = new BitSet(rules.size()); 364 ruleCandidates.or(remaining); 365 366 final RuleCandidatesIterator candidatesIterator = new RuleCandidatesIterator(ruleCandidates); 367 osm.visitKeys(candidatesIterator); 368 candidatesIterator.prepare(); 369 return candidatesIterator; 370 } 371 372 /** 373 * Clear the index. 374 * <p> 375 * You must own the write lock STYLE_SOURCE_LOCK when calling this method. 376 */ 377 public void clear() { 378 rules.clear(); 379 index.clear(); 380 remaining.clear(); 381 } 382 } 383 384 /** 385 * Constructs a new, active {@link MapCSSStyleSource}. 386 * @param url URL that {@link org.openstreetmap.josm.io.CachedFile} understands 387 * @param name The name for this StyleSource 388 * @param shortdescription The title for that source. 389 */ 390 public MapCSSStyleSource(String url, String name, String shortdescription) { 391 super(url, name, shortdescription); 392 } 393 394 /** 395 * Constructs a new {@link MapCSSStyleSource} 396 * @param entry The entry to copy the data (url, name, ...) from. 397 */ 398 public MapCSSStyleSource(SourceEntry entry) { 399 super(entry); 400 } 401 402 /** 403 * <p>Creates a new style source from the MapCSS styles supplied in 404 * {@code css}</p> 405 * 406 * @param css the MapCSS style declaration. Must not be null. 407 * @throws IllegalArgumentException if {@code css} is null 408 */ 409 public MapCSSStyleSource(String css) { 410 super(null, null, null); 411 CheckParameterUtil.ensureParameterNotNull(css); 412 this.css = css; 413 } 414 415 @Override 416 public void loadStyleSource(boolean metadataOnly) { 417 STYLE_SOURCE_LOCK.writeLock().lock(); 418 try { 419 init(); 420 rules.clear(); 421 nodeRules.clear(); 422 wayRules.clear(); 423 wayNoAreaRules.clear(); 424 relationRules.clear(); 425 multipolygonRules.clear(); 426 canvasRules.clear(); 427 try (InputStream in = getSourceInputStream()) { 428 try { 429 // evaluate @media { ... } blocks 430 MapCSSParser preprocessor = new MapCSSParser(in, "UTF-8", MapCSSParser.LexicalState.PREPROCESSOR); 431 String mapcss = preprocessor.pp_root(this); 432 433 // do the actual mapcss parsing 434 InputStream in2 = new ByteArrayInputStream(mapcss.getBytes(StandardCharsets.UTF_8)); 435 MapCSSParser parser = new MapCSSParser(in2, "UTF-8", MapCSSParser.LexicalState.DEFAULT); 436 parser.sheet(this); 437 438 loadMeta(); 439 if (!metadataOnly) { 440 loadCanvas(); 441 loadSettings(); 442 } 443 // remove "areaStyle" pseudo classes intended only for validator (causes StackOverflowError otherwise) 444 removeAreaStyleClasses(); 445 } finally { 446 closeSourceInputStream(in); 447 } 448 } catch (IOException e) { 449 Logging.warn(tr("Failed to load Mappaint styles from ''{0}''. Exception was: {1}", url, e.toString())); 450 Logging.log(Logging.LEVEL_ERROR, e); 451 logError(e); 452 } catch (TokenMgrError e) { 453 Logging.warn(tr("Failed to parse Mappaint styles from ''{0}''. Error was: {1}", url, e.getMessage())); 454 Logging.error(e); 455 logError(e); 456 } catch (ParseException e) { 457 Logging.warn(tr("Failed to parse Mappaint styles from ''{0}''. Error was: {1}", url, e.getMessage())); 458 Logging.error(e); 459 logError(new ParseException(e.getMessage())); // allow e to be garbage collected, it links to the entire token stream 460 } 461 if (metadataOnly) { 462 return; 463 } 464 // optimization: filter rules for different primitive types 465 for (MapCSSRule r: rules) { 466 // find the rightmost selector, this must be a GeneralSelector 467 Selector selRightmost = r.selector; 468 while (selRightmost instanceof ChildOrParentSelector) { 469 selRightmost = ((ChildOrParentSelector) selRightmost).right; 470 } 471 MapCSSRule optRule = new MapCSSRule(r.selector.optimizedBaseCheck(), r.declaration); 472 final String base = ((GeneralSelector) selRightmost).getBase(); 473 switch (base) { 474 case "node": 475 nodeRules.add(optRule); 476 break; 477 case "way": 478 wayNoAreaRules.add(optRule); 479 wayRules.add(optRule); 480 break; 481 case "area": 482 wayRules.add(optRule); 483 multipolygonRules.add(optRule); 484 break; 485 case "relation": 486 relationRules.add(optRule); 487 multipolygonRules.add(optRule); 488 break; 489 case "*": 490 nodeRules.add(optRule); 491 wayRules.add(optRule); 492 wayNoAreaRules.add(optRule); 493 relationRules.add(optRule); 494 multipolygonRules.add(optRule); 495 break; 496 case "canvas": 497 canvasRules.add(r); 498 break; 499 case "meta": 500 case "setting": 501 break; 502 default: 503 final RuntimeException e = new JosmRuntimeException(MessageFormat.format("Unknown MapCSS base selector {0}", base)); 504 Logging.warn(tr("Failed to parse Mappaint styles from ''{0}''. Error was: {1}", url, e.getMessage())); 505 Logging.error(e); 506 logError(e); 507 } 508 } 509 nodeRules.initIndex(); 510 wayRules.initIndex(); 511 wayNoAreaRules.initIndex(); 512 relationRules.initIndex(); 513 multipolygonRules.initIndex(); 514 canvasRules.initIndex(); 515 loaded = true; 516 } finally { 517 STYLE_SOURCE_LOCK.writeLock().unlock(); 518 } 519 } 520 521 @Override 522 public InputStream getSourceInputStream() throws IOException { 523 if (css != null) { 524 return new ByteArrayInputStream(css.getBytes(StandardCharsets.UTF_8)); 525 } 526 CachedFile cf = getCachedFile(); 527 if (isZip) { 528 File file = cf.getFile(); 529 zipFile = new ZipFile(file, StandardCharsets.UTF_8); 530 zipIcons = file; 531 I18n.addTexts(zipIcons); 532 ZipEntry zipEntry = zipFile.getEntry(zipEntryPath); 533 return zipFile.getInputStream(zipEntry); 534 } else { 535 zipFile = null; 536 zipIcons = null; 537 return cf.getInputStream(); 538 } 539 } 540 541 @Override 542 @SuppressWarnings("resource") 543 public CachedFile getCachedFile() throws IOException { 544 return new CachedFile(url).setHttpAccept(MAPCSS_STYLE_MIME_TYPES); // NOSONAR 545 } 546 547 @Override 548 public void closeSourceInputStream(InputStream is) { 549 super.closeSourceInputStream(is); 550 if (isZip) { 551 Utils.close(zipFile); 552 } 553 } 554 555 /** 556 * load meta info from a selector "meta" 557 */ 558 private void loadMeta() { 559 Cascade c = constructSpecial("meta"); 560 String pTitle = c.get("title", null, String.class); 561 if (title == null) { 562 title = pTitle; 563 } 564 String pIcon = c.get("icon", null, String.class); 565 if (icon == null) { 566 icon = pIcon; 567 } 568 } 569 570 private void loadCanvas() { 571 Cascade c = constructSpecial("canvas"); 572 backgroundColorOverride = c.get("fill-color", null, Color.class); 573 } 574 575 private void loadSettings() { 576 settings.clear(); 577 settingValues.clear(); 578 MultiCascade mc = new MultiCascade(); 579 Node n = new Node(); 580 String code = LanguageInfo.getJOSMLocaleCode(); 581 n.put("lang", code); 582 // create a fake environment to read the meta data block 583 Environment env = new Environment(n, mc, "default", this); 584 585 for (MapCSSRule r : rules) { 586 if (r.selector instanceof GeneralSelector) { 587 GeneralSelector gs = (GeneralSelector) r.selector; 588 if ("setting".equals(gs.getBase())) { 589 if (!gs.matchesConditions(env)) { 590 continue; 591 } 592 env.layer = null; 593 env.layer = gs.getSubpart().getId(env); 594 r.execute(env); 595 } 596 } 597 } 598 for (Entry<String, Cascade> e : mc.getLayers()) { 599 if ("default".equals(e.getKey())) { 600 Logging.warn("setting requires layer identifier e.g. 'setting::my_setting {...}'"); 601 continue; 602 } 603 Cascade c = e.getValue(); 604 String type = c.get("type", null, String.class); 605 StyleSetting set = null; 606 if ("boolean".equals(type)) { 607 set = BooleanStyleSetting.create(c, this, e.getKey()); 608 } else { 609 Logging.warn("Unkown setting type: "+type); 610 } 611 if (set != null) { 612 settings.add(set); 613 settingValues.put(e.getKey(), set.getValue()); 614 } 615 } 616 settings.sort(null); 617 } 618 619 private Cascade constructSpecial(String type) { 620 621 MultiCascade mc = new MultiCascade(); 622 Node n = new Node(); 623 String code = LanguageInfo.getJOSMLocaleCode(); 624 n.put("lang", code); 625 // create a fake environment to read the meta data block 626 Environment env = new Environment(n, mc, "default", this); 627 628 for (MapCSSRule r : rules) { 629 if (r.selector instanceof GeneralSelector) { 630 GeneralSelector gs = (GeneralSelector) r.selector; 631 if (gs.getBase().equals(type)) { 632 if (!gs.matchesConditions(env)) { 633 continue; 634 } 635 r.execute(env); 636 } 637 } 638 } 639 return mc.getCascade("default"); 640 } 641 642 @Override 643 public Color getBackgroundColorOverride() { 644 return backgroundColorOverride; 645 } 646 647 @Override 648 public void apply(MultiCascade mc, IPrimitive osm, double scale, boolean pretendWayIsClosed) { 649 MapCSSRuleIndex matchingRuleIndex; 650 if (osm instanceof INode) { 651 matchingRuleIndex = nodeRules; 652 } else if (osm instanceof IWay) { 653 if (OsmUtils.isFalse(osm.get("area"))) { 654 matchingRuleIndex = wayNoAreaRules; 655 } else { 656 matchingRuleIndex = wayRules; 657 } 658 } else if (osm instanceof IRelation) { 659 if (((IRelation<?>) osm).isMultipolygon()) { 660 matchingRuleIndex = multipolygonRules; 661 } else if (osm.hasKey("#canvas")) { 662 matchingRuleIndex = canvasRules; 663 } else { 664 matchingRuleIndex = relationRules; 665 } 666 } else { 667 throw new IllegalArgumentException("Unsupported type: " + osm); 668 } 669 670 Environment env = new Environment(osm, mc, null, this); 671 // the declaration indices are sorted, so it suffices to save the last used index 672 int lastDeclUsed = -1; 673 674 Iterator<MapCSSRule> candidates = matchingRuleIndex.getRuleCandidates(osm); 675 while (candidates.hasNext()) { 676 MapCSSRule r = candidates.next(); 677 env.clearSelectorMatchingInformation(); 678 env.layer = r.selector.getSubpart().getId(env); 679 String sub = env.layer; 680 if (r.selector.matches(env)) { // as side effect env.parent will be set (if s is a child selector) 681 Selector s = r.selector; 682 if (s.getRange().contains(scale)) { 683 mc.range = Range.cut(mc.range, s.getRange()); 684 } else { 685 mc.range = mc.range.reduceAround(scale, s.getRange()); 686 continue; 687 } 688 689 if (r.declaration.idx == lastDeclUsed) 690 continue; // don't apply one declaration more than once 691 lastDeclUsed = r.declaration.idx; 692 if ("*".equals(sub)) { 693 for (Entry<String, Cascade> entry : mc.getLayers()) { 694 env.layer = entry.getKey(); 695 if ("*".equals(env.layer)) { 696 continue; 697 } 698 r.execute(env); 699 } 700 } 701 env.layer = sub; 702 r.execute(env); 703 } 704 } 705 } 706 707 /** 708 * Evaluate a supports condition 709 * @param feature The feature to evaluate for 710 * @param val The additional parameter passed to evaluate 711 * @return <code>true</code> if JSOM supports that feature 712 */ 713 public boolean evalSupportsDeclCondition(String feature, Object val) { 714 if (feature == null) return false; 715 if (SUPPORTED_KEYS.contains(feature)) return true; 716 switch (feature) { 717 case "user-agent": 718 String s = Cascade.convertTo(val, String.class); 719 return "josm".equals(s); 720 case "min-josm-version": 721 Float min = Cascade.convertTo(val, Float.class); 722 return min != null && Math.round(min) <= Version.getInstance().getVersion(); 723 case "max-josm-version": 724 Float max = Cascade.convertTo(val, Float.class); 725 return max != null && Math.round(max) >= Version.getInstance().getVersion(); 726 default: 727 return false; 728 } 729 } 730 731 /** 732 * Removes "meta" rules. Not needed for validator. 733 * @since 13633 734 */ 735 public void removeMetaRules() { 736 for (Iterator<MapCSSRule> it = rules.iterator(); it.hasNext();) { 737 MapCSSRule x = it.next(); 738 if (x.selector instanceof GeneralSelector) { 739 GeneralSelector gs = (GeneralSelector) x.selector; 740 if ("meta".equals(gs.base)) { 741 it.remove(); 742 } 743 } 744 } 745 } 746 747 /** 748 * Removes "areaStyle" pseudo-classes. Only needed for validator. 749 * @since 13633 750 */ 751 public void removeAreaStyleClasses() { 752 for (Iterator<MapCSSRule> it = rules.iterator(); it.hasNext();) { 753 removeAreaStyleClasses(it.next().selector); 754 } 755 } 756 757 private static void removeAreaStyleClasses(Selector sel) { 758 if (sel instanceof ChildOrParentSelector) { 759 removeAreaStyleClasses((ChildOrParentSelector) sel); 760 } else if (sel instanceof AbstractSelector) { 761 removeAreaStyleClasses((AbstractSelector) sel); 762 } 763 } 764 765 private static void removeAreaStyleClasses(ChildOrParentSelector sel) { 766 removeAreaStyleClasses(sel.left); 767 removeAreaStyleClasses(sel.right); 768 } 769 770 private static void removeAreaStyleClasses(AbstractSelector sel) { 771 if (sel.conds != null) { 772 for (Iterator<Condition> it = sel.conds.iterator(); it.hasNext();) { 773 Condition c = it.next(); 774 if (c instanceof PseudoClassCondition) { 775 PseudoClassCondition cc = (PseudoClassCondition) c; 776 if ("areaStyle".equals(cc.method.getName())) { 777 Logging.warn("Removing 'areaStyle' pseudo-class from "+sel+". This class is only meant for validator"); 778 it.remove(); 779 } 780 } 781 } 782 } 783 } 784 785 @Override 786 public String toString() { 787 return Utils.join("\n", rules); 788 } 789}