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