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.stream.Collectors; 032import java.util.zip.ZipEntry; 033import java.util.zip.ZipFile; 034 035import org.openstreetmap.josm.data.Version; 036import org.openstreetmap.josm.data.osm.INode; 037import org.openstreetmap.josm.data.osm.IPrimitive; 038import org.openstreetmap.josm.data.osm.IRelation; 039import org.openstreetmap.josm.data.osm.IWay; 040import org.openstreetmap.josm.data.osm.KeyValueVisitor; 041import org.openstreetmap.josm.data.osm.Node; 042import org.openstreetmap.josm.data.osm.OsmUtils; 043import org.openstreetmap.josm.data.osm.Tagged; 044import org.openstreetmap.josm.data.preferences.sources.SourceEntry; 045import org.openstreetmap.josm.gui.mappaint.Cascade; 046import org.openstreetmap.josm.gui.mappaint.Environment; 047import org.openstreetmap.josm.gui.mappaint.MultiCascade; 048import org.openstreetmap.josm.gui.mappaint.Range; 049import org.openstreetmap.josm.gui.mappaint.StyleKeys; 050import org.openstreetmap.josm.gui.mappaint.StyleSetting; 051import org.openstreetmap.josm.gui.mappaint.StyleSetting.StyleSettingGroup; 052import org.openstreetmap.josm.gui.mappaint.StyleSettingFactory; 053import org.openstreetmap.josm.gui.mappaint.StyleSource; 054import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.KeyCondition; 055import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.KeyMatchType; 056import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.KeyValueCondition; 057import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.PseudoClassCondition; 058import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.SimpleKeyValueCondition; 059import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.AbstractSelector; 060import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.ChildOrParentSelector; 061import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector; 062import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.OptimizedGeneralSelector; 063import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser; 064import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException; 065import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.TokenMgrError; 066import org.openstreetmap.josm.gui.mappaint.styleelement.LineElement; 067import org.openstreetmap.josm.io.CachedFile; 068import org.openstreetmap.josm.io.UTFInputStreamReader; 069import org.openstreetmap.josm.tools.CheckParameterUtil; 070import org.openstreetmap.josm.tools.I18n; 071import org.openstreetmap.josm.tools.JosmRuntimeException; 072import org.openstreetmap.josm.tools.LanguageInfo; 073import org.openstreetmap.josm.tools.Logging; 074import org.openstreetmap.josm.tools.Utils; 075 076/** 077 * This is a mappaint style that is based on MapCSS rules. 078 */ 079public class MapCSSStyleSource extends StyleSource { 080 081 /** 082 * The accepted MIME types sent in the HTTP Accept header. 083 * @since 6867 084 */ 085 public static final String MAPCSS_STYLE_MIME_TYPES = 086 "text/x-mapcss, text/mapcss, text/css; q=0.9, text/plain; q=0.8, application/zip, application/octet-stream; q=0.5"; 087 088 /** 089 * all rules in this style file 090 */ 091 public final List<MapCSSRule> rules = new ArrayList<>(); 092 /** 093 * Rules for nodes 094 */ 095 public final MapCSSRuleIndex nodeRules = new MapCSSRuleIndex(); 096 /** 097 * Rules for ways without tag area=no 098 */ 099 public final MapCSSRuleIndex wayRules = new MapCSSRuleIndex(); 100 /** 101 * Rules for ways with tag area=no 102 */ 103 public final MapCSSRuleIndex wayNoAreaRules = new MapCSSRuleIndex(); 104 /** 105 * Rules for relations that are not multipolygon relations 106 */ 107 public final MapCSSRuleIndex relationRules = new MapCSSRuleIndex(); 108 /** 109 * Rules for multipolygon relations 110 */ 111 public final MapCSSRuleIndex multipolygonRules = new MapCSSRuleIndex(); 112 /** 113 * rules to apply canvas properties 114 */ 115 public final MapCSSRuleIndex canvasRules = new MapCSSRuleIndex(); 116 117 private Color backgroundColorOverride; 118 private String css; 119 private ZipFile zipFile; 120 121 /** 122 * This lock prevents concurrent execution of {@link MapCSSRuleIndex#clear() } / 123 * {@link MapCSSRuleIndex#initIndex()} and {@link MapCSSRuleIndex#getRuleCandidates }. 124 * 125 * For efficiency reasons, these methods are synchronized higher up the 126 * stack trace. 127 */ 128 public static final ReadWriteLock STYLE_SOURCE_LOCK = new ReentrantReadWriteLock(); 129 130 /** 131 * Set of all supported MapCSS keys. 132 */ 133 static final Set<String> SUPPORTED_KEYS = new HashSet<>(); 134 static { 135 Field[] declaredFields = StyleKeys.class.getDeclaredFields(); 136 for (Field f : declaredFields) { 137 try { 138 SUPPORTED_KEYS.add((String) f.get(null)); 139 if (!f.getName().toLowerCase(Locale.ENGLISH).replace('_', '-').equals(f.get(null))) { 140 throw new JosmRuntimeException(f.getName()); 141 } 142 } catch (IllegalArgumentException | IllegalAccessException ex) { 143 throw new JosmRuntimeException(ex); 144 } 145 } 146 for (LineElement.LineType lt : LineElement.LineType.values()) { 147 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.COLOR); 148 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES); 149 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES_BACKGROUND_COLOR); 150 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES_BACKGROUND_OPACITY); 151 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES_OFFSET); 152 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.LINECAP); 153 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.LINEJOIN); 154 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.MITERLIMIT); 155 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.OFFSET); 156 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.OPACITY); 157 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.REAL_WIDTH); 158 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.WIDTH); 159 } 160 } 161 162 /** 163 * A collection of {@link MapCSSRule}s, that are indexed by tag key and value. 164 * 165 * Speeds up the process of finding all rules that match a certain primitive. 166 * 167 * Rules with a {@link SimpleKeyValueCondition} [key=value] or rules that require a specific key to be set are 168 * indexed. Now you only need to loop the tags of a primitive to retrieve the possibly matching rules. 169 * 170 * To use this index, you need to {@link #add(MapCSSRule)} all rules to it. You then need to call 171 * {@link #initIndex()}. Afterwards, you can use {@link #getRuleCandidates(IPrimitive)} to get an iterator over 172 * all rules that might be applied to that primitive. 173 */ 174 public static class MapCSSRuleIndex { 175 /** 176 * This is an iterator over all rules that are marked as possible in the bitset. 177 * 178 * @author Michael Zangl 179 */ 180 private final class RuleCandidatesIterator implements Iterator<MapCSSRule>, KeyValueVisitor { 181 private final BitSet ruleCandidates; 182 private int next; 183 184 private RuleCandidatesIterator(BitSet ruleCandidates) { 185 this.ruleCandidates = ruleCandidates; 186 } 187 188 @Override 189 public boolean hasNext() { 190 return next >= 0 && next < rules.size(); 191 } 192 193 @Override 194 public MapCSSRule next() { 195 if (!hasNext()) 196 throw new NoSuchElementException(); 197 MapCSSRule rule = rules.get(next); 198 next = ruleCandidates.nextSetBit(next + 1); 199 return rule; 200 } 201 202 @Override 203 public void remove() { 204 throw new UnsupportedOperationException(); 205 } 206 207 @Override 208 public void visitKeyValue(Tagged p, String key, String value) { 209 MapCSSKeyRules v = index.get(key); 210 if (v != null) { 211 BitSet rs = v.get(value); 212 ruleCandidates.or(rs); 213 } 214 } 215 216 /** 217 * Call this before using the iterator. 218 */ 219 public void prepare() { 220 next = ruleCandidates.nextSetBit(0); 221 } 222 } 223 224 /** 225 * This is a map of all rules that are only applied if the primitive has a given key (and possibly value) 226 * 227 * @author Michael Zangl 228 */ 229 private static final class MapCSSKeyRules { 230 /** 231 * The indexes of rules that might be applied if this tag is present and the value has no special handling. 232 */ 233 BitSet generalRules = new BitSet(); 234 235 /** 236 * A map that sores the indexes of rules that might be applied if the key=value pair is present on this 237 * primitive. This includes all key=* rules. 238 */ 239 Map<String, BitSet> specialRules = new HashMap<>(); 240 241 public void addForKey(int ruleIndex) { 242 generalRules.set(ruleIndex); 243 for (BitSet r : specialRules.values()) { 244 r.set(ruleIndex); 245 } 246 } 247 248 public void addForKeyAndValue(String value, int ruleIndex) { 249 BitSet forValue = specialRules.get(value); 250 if (forValue == null) { 251 forValue = new BitSet(); 252 forValue.or(generalRules); 253 specialRules.put(value.intern(), forValue); 254 } 255 forValue.set(ruleIndex); 256 } 257 258 public BitSet get(String value) { 259 BitSet forValue = specialRules.get(value); 260 if (forValue != null) return forValue; else return generalRules; 261 } 262 } 263 264 /** 265 * All rules this index is for. Once this index is built, this list is sorted. 266 */ 267 private final List<MapCSSRule> rules = new ArrayList<>(); 268 /** 269 * All rules that only apply when the given key is present. 270 */ 271 private final Map<String, MapCSSKeyRules> index = new HashMap<>(); 272 /** 273 * Rules that do not require any key to be present. Only the index in the {@link #rules} array is stored. 274 */ 275 private final BitSet remaining = new BitSet(); 276 277 /** 278 * Add a rule to this index. This needs to be called before {@link #initIndex()} is called. 279 * @param rule The rule to add. 280 */ 281 public void add(MapCSSRule rule) { 282 rules.add(rule); 283 } 284 285 /** 286 * Initialize the index. 287 * <p> 288 * You must own the write lock of STYLE_SOURCE_LOCK when calling this method. 289 */ 290 public void initIndex() { 291 Collections.sort(rules); 292 for (int ruleIndex = 0; ruleIndex < rules.size(); ruleIndex++) { 293 MapCSSRule r = rules.get(ruleIndex); 294 // find the rightmost selector, this must be a GeneralSelector 295 Selector selRightmost = r.selector; 296 while (selRightmost instanceof ChildOrParentSelector) { 297 selRightmost = ((ChildOrParentSelector) selRightmost).right; 298 } 299 OptimizedGeneralSelector s = (OptimizedGeneralSelector) selRightmost; 300 if (s.conds == null) { 301 remaining.set(ruleIndex); 302 continue; 303 } 304 List<SimpleKeyValueCondition> sk = new ArrayList<>(Utils.filteredCollection(s.conds, 305 SimpleKeyValueCondition.class)); 306 if (!sk.isEmpty()) { 307 SimpleKeyValueCondition c = sk.get(sk.size() - 1); 308 getEntryInIndex(c.k).addForKeyAndValue(c.v, ruleIndex); 309 } else { 310 String key = findAnyRequiredKey(s.conds); 311 if (key != null) { 312 getEntryInIndex(key).addForKey(ruleIndex); 313 } else { 314 remaining.set(ruleIndex); 315 } 316 } 317 } 318 } 319 320 /** 321 * Search for any key that condition might depend on. 322 * 323 * @param conds The conditions to search through. 324 * @return An arbitrary key this rule depends on or <code>null</code> if there is no such key. 325 */ 326 private static String findAnyRequiredKey(List<Condition> conds) { 327 String key = null; 328 for (Condition c : conds) { 329 if (c instanceof KeyCondition) { 330 KeyCondition keyCondition = (KeyCondition) c; 331 if (!keyCondition.negateResult && conditionRequiresKeyPresence(keyCondition.matchType)) { 332 key = keyCondition.label; 333 } 334 } else if (c instanceof KeyValueCondition) { 335 KeyValueCondition keyValueCondition = (KeyValueCondition) c; 336 if (keyValueCondition.requiresExactKeyMatch()) { 337 key = keyValueCondition.k; 338 } 339 } 340 } 341 return key; 342 } 343 344 private static boolean conditionRequiresKeyPresence(KeyMatchType matchType) { 345 return matchType != KeyMatchType.REGEX; 346 } 347 348 private MapCSSKeyRules getEntryInIndex(String key) { 349 MapCSSKeyRules rulesWithMatchingKey = index.get(key); 350 if (rulesWithMatchingKey == null) { 351 rulesWithMatchingKey = new MapCSSKeyRules(); 352 index.put(key.intern(), rulesWithMatchingKey); 353 } 354 return rulesWithMatchingKey; 355 } 356 357 /** 358 * Get a subset of all rules that might match the primitive. Rules not included in the result are guaranteed to 359 * not match this primitive. 360 * <p> 361 * You must have a read lock of STYLE_SOURCE_LOCK when calling this method. 362 * 363 * @param osm the primitive to match 364 * @return An iterator over possible rules in the right order. 365 * @since 13810 (signature) 366 */ 367 public Iterator<MapCSSRule> getRuleCandidates(IPrimitive osm) { 368 final BitSet ruleCandidates = new BitSet(rules.size()); 369 ruleCandidates.or(remaining); 370 371 final RuleCandidatesIterator candidatesIterator = new RuleCandidatesIterator(ruleCandidates); 372 osm.visitKeys(candidatesIterator); 373 candidatesIterator.prepare(); 374 return candidatesIterator; 375 } 376 377 /** 378 * Clear the index. 379 * <p> 380 * You must own the write lock STYLE_SOURCE_LOCK when calling this method. 381 */ 382 public void clear() { 383 rules.clear(); 384 index.clear(); 385 remaining.clear(); 386 } 387 } 388 389 /** 390 * Constructs a new, active {@link MapCSSStyleSource}. 391 * @param url URL that {@link org.openstreetmap.josm.io.CachedFile} understands 392 * @param name The name for this StyleSource 393 * @param shortdescription The title for that source. 394 */ 395 public MapCSSStyleSource(String url, String name, String shortdescription) { 396 super(url, name, shortdescription); 397 } 398 399 /** 400 * Constructs a new {@link MapCSSStyleSource} 401 * @param entry The entry to copy the data (url, name, ...) from. 402 */ 403 public MapCSSStyleSource(SourceEntry entry) { 404 super(entry); 405 } 406 407 /** 408 * <p>Creates a new style source from the MapCSS styles supplied in 409 * {@code css}</p> 410 * 411 * @param css the MapCSS style declaration. Must not be null. 412 * @throws IllegalArgumentException if {@code css} is null 413 */ 414 public MapCSSStyleSource(String css) { 415 super(null, null, null); 416 CheckParameterUtil.ensureParameterNotNull(css); 417 this.css = css; 418 } 419 420 @Override 421 public void loadStyleSource(boolean metadataOnly) { 422 STYLE_SOURCE_LOCK.writeLock().lock(); 423 try { 424 init(); 425 rules.clear(); 426 nodeRules.clear(); 427 wayRules.clear(); 428 wayNoAreaRules.clear(); 429 relationRules.clear(); 430 multipolygonRules.clear(); 431 canvasRules.clear(); 432 try (InputStream in = getSourceInputStream()) { 433 try (Reader reader = new BufferedReader(UTFInputStreamReader.create(in))) { 434 // evaluate @media { ... } blocks 435 MapCSSParser preprocessor = new MapCSSParser(reader, MapCSSParser.LexicalState.PREPROCESSOR); 436 437 // do the actual mapcss parsing 438 try (Reader in2 = new StringReader(preprocessor.pp_root(this))) { 439 new MapCSSParser(in2, MapCSSParser.LexicalState.DEFAULT).sheet(this); 440 } 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 StyleSetting set = StyleSettingFactory.create(c, this, e.getKey()); 627 if (set != null) { 628 settings.add(set); 629 settingValues.put(e.getKey(), set.getValue()); 630 String groupId = c.get("group", null, String.class); 631 if (groupId != null) { 632 final StyleSettingGroup group = settingGroups.keySet().stream() 633 .filter(g -> g.key.equals(groupId)) 634 .findAny() 635 .orElseThrow(() -> new IllegalArgumentException("Unknown settings group: " + groupId)); 636 settingGroups.get(group).add(set); 637 } 638 } 639 } 640 settings.sort(null); 641 } 642 643 private Cascade constructSpecial(String type) { 644 645 MultiCascade mc = new MultiCascade(); 646 Node n = new Node(); 647 String code = LanguageInfo.getJOSMLocaleCode(); 648 n.put("lang", code); 649 // create a fake environment to read the meta data block 650 Environment env = new Environment(n, mc, "default", this); 651 652 for (MapCSSRule r : rules) { 653 if (r.selector instanceof GeneralSelector) { 654 GeneralSelector gs = (GeneralSelector) r.selector; 655 if (gs.getBase().equals(type)) { 656 if (!gs.matchesConditions(env)) { 657 continue; 658 } 659 r.execute(env); 660 } 661 } 662 } 663 return mc.getCascade("default"); 664 } 665 666 @Override 667 public Color getBackgroundColorOverride() { 668 return backgroundColorOverride; 669 } 670 671 @Override 672 public void apply(MultiCascade mc, IPrimitive osm, double scale, boolean pretendWayIsClosed) { 673 MapCSSRuleIndex matchingRuleIndex; 674 if (osm instanceof INode) { 675 matchingRuleIndex = nodeRules; 676 } else if (osm instanceof IWay) { 677 if (OsmUtils.isFalse(osm.get("area"))) { 678 matchingRuleIndex = wayNoAreaRules; 679 } else { 680 matchingRuleIndex = wayRules; 681 } 682 } else if (osm instanceof IRelation) { 683 if (((IRelation<?>) osm).isMultipolygon()) { 684 matchingRuleIndex = multipolygonRules; 685 } else if (osm.hasKey("#canvas")) { 686 matchingRuleIndex = canvasRules; 687 } else { 688 matchingRuleIndex = relationRules; 689 } 690 } else { 691 throw new IllegalArgumentException("Unsupported type: " + osm); 692 } 693 694 Environment env = new Environment(osm, mc, null, this); 695 // the declaration indices are sorted, so it suffices to save the last used index 696 int lastDeclUsed = -1; 697 698 Iterator<MapCSSRule> candidates = matchingRuleIndex.getRuleCandidates(osm); 699 while (candidates.hasNext()) { 700 MapCSSRule r = candidates.next(); 701 env.clearSelectorMatchingInformation(); 702 env.layer = r.selector.getSubpart().getId(env); 703 String sub = env.layer; 704 if (r.selector.matches(env)) { // as side effect env.parent will be set (if s is a child selector) 705 Selector s = r.selector; 706 if (s.getRange().contains(scale)) { 707 mc.range = Range.cut(mc.range, s.getRange()); 708 } else { 709 mc.range = mc.range.reduceAround(scale, s.getRange()); 710 continue; 711 } 712 713 if (r.declaration.idx == lastDeclUsed) 714 continue; // don't apply one declaration more than once 715 lastDeclUsed = r.declaration.idx; 716 if ("*".equals(sub)) { 717 for (Entry<String, Cascade> entry : mc.getLayers()) { 718 env.layer = entry.getKey(); 719 if ("*".equals(env.layer)) { 720 continue; 721 } 722 r.execute(env); 723 } 724 } 725 env.layer = sub; 726 r.execute(env); 727 } 728 } 729 } 730 731 /** 732 * Evaluate a supports condition 733 * @param feature The feature to evaluate for 734 * @param val The additional parameter passed to evaluate 735 * @return <code>true</code> if JSOM supports that feature 736 */ 737 public boolean evalSupportsDeclCondition(String feature, Object val) { 738 if (feature == null) return false; 739 if (SUPPORTED_KEYS.contains(feature)) return true; 740 switch (feature) { 741 case "user-agent": 742 String s = Cascade.convertTo(val, String.class); 743 return "josm".equals(s); 744 case "min-josm-version": 745 Float min = Cascade.convertTo(val, Float.class); 746 return min != null && Math.round(min) <= Version.getInstance().getVersion(); 747 case "max-josm-version": 748 Float max = Cascade.convertTo(val, Float.class); 749 return max != null && Math.round(max) >= Version.getInstance().getVersion(); 750 default: 751 return false; 752 } 753 } 754 755 /** 756 * Removes "meta" rules. Not needed for validator. 757 * @since 13633 758 */ 759 public void removeMetaRules() { 760 for (Iterator<MapCSSRule> it = rules.iterator(); it.hasNext();) { 761 MapCSSRule x = it.next(); 762 if (x.selector instanceof GeneralSelector) { 763 GeneralSelector gs = (GeneralSelector) x.selector; 764 if (Selector.BASE_META.equals(gs.base)) { 765 it.remove(); 766 } 767 } 768 } 769 } 770 771 /** 772 * Removes "areaStyle" pseudo-classes. Only needed for validator. 773 * @since 13633 774 */ 775 public void removeAreaStyleClasses() { 776 for (Iterator<MapCSSRule> it = rules.iterator(); it.hasNext();) { 777 removeAreaStyleClasses(it.next().selector); 778 } 779 } 780 781 private static void removeAreaStyleClasses(Selector sel) { 782 if (sel instanceof ChildOrParentSelector) { 783 removeAreaStyleClasses((ChildOrParentSelector) sel); 784 } else if (sel instanceof AbstractSelector) { 785 removeAreaStyleClasses((AbstractSelector) sel); 786 } 787 } 788 789 private static void removeAreaStyleClasses(ChildOrParentSelector sel) { 790 removeAreaStyleClasses(sel.left); 791 removeAreaStyleClasses(sel.right); 792 } 793 794 private static void removeAreaStyleClasses(AbstractSelector sel) { 795 if (sel.conds != null) { 796 for (Iterator<Condition> it = sel.conds.iterator(); it.hasNext();) { 797 Condition c = it.next(); 798 if (c instanceof PseudoClassCondition) { 799 PseudoClassCondition cc = (PseudoClassCondition) c; 800 if ("areaStyle".equals(cc.method.getName())) { 801 Logging.warn("Removing 'areaStyle' pseudo-class from "+sel+". This class is only meant for validator"); 802 it.remove(); 803 } 804 } 805 } 806 } 807 } 808 809 @Override 810 public String toString() { 811 return rules.stream().map(MapCSSRule::toString).collect(Collectors.joining("\n")); 812 } 813}