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