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