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