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