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