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