source: josm/trunk/src/org/openstreetmap/josm/gui/mappaint/mapcss/MapCSSStyleSource.java@ 16000

Last change on this file since 16000 was 16000, checked in by simon04, 4 years ago

see #18802 - fix MapCSSRendererTest.testRender[relation-linkselector]

  • Property svn:eol-style set to native
File size: 26.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.mappaint.mapcss;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Color;
7import java.io.BufferedReader;
8import java.io.ByteArrayInputStream;
9import java.io.File;
10import java.io.IOException;
11import java.io.InputStream;
12import java.io.Reader;
13import java.io.StringReader;
14import java.lang.reflect.Field;
15import java.nio.charset.StandardCharsets;
16import java.util.ArrayList;
17import java.util.BitSet;
18import java.util.Collections;
19import java.util.HashMap;
20import java.util.HashSet;
21import java.util.Iterator;
22import java.util.List;
23import java.util.Locale;
24import java.util.Map;
25import java.util.Map.Entry;
26import java.util.NoSuchElementException;
27import java.util.Optional;
28import java.util.Set;
29import java.util.concurrent.locks.ReadWriteLock;
30import java.util.concurrent.locks.ReentrantReadWriteLock;
31import java.util.stream.Collectors;
32import java.util.zip.ZipEntry;
33import java.util.zip.ZipFile;
34
35import org.openstreetmap.josm.data.Version;
36import org.openstreetmap.josm.data.osm.IPrimitive;
37import org.openstreetmap.josm.data.osm.KeyValueVisitor;
38import org.openstreetmap.josm.data.osm.Node;
39import org.openstreetmap.josm.data.osm.Tagged;
40import org.openstreetmap.josm.data.preferences.sources.SourceEntry;
41import org.openstreetmap.josm.gui.mappaint.Cascade;
42import org.openstreetmap.josm.gui.mappaint.Environment;
43import org.openstreetmap.josm.gui.mappaint.MultiCascade;
44import org.openstreetmap.josm.gui.mappaint.Range;
45import org.openstreetmap.josm.gui.mappaint.StyleKeys;
46import org.openstreetmap.josm.gui.mappaint.StyleSetting;
47import org.openstreetmap.josm.gui.mappaint.StyleSetting.StyleSettingGroup;
48import org.openstreetmap.josm.gui.mappaint.StyleSettingFactory;
49import org.openstreetmap.josm.gui.mappaint.StyleSource;
50import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.KeyCondition;
51import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.KeyMatchType;
52import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.KeyValueCondition;
53import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.SimpleKeyValueCondition;
54import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector;
55import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser;
56import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException;
57import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.TokenMgrError;
58import org.openstreetmap.josm.gui.mappaint.styleelement.LineElement;
59import org.openstreetmap.josm.io.CachedFile;
60import org.openstreetmap.josm.io.UTFInputStreamReader;
61import org.openstreetmap.josm.tools.CheckParameterUtil;
62import org.openstreetmap.josm.tools.I18n;
63import org.openstreetmap.josm.tools.JosmRuntimeException;
64import org.openstreetmap.josm.tools.LanguageInfo;
65import org.openstreetmap.josm.tools.Logging;
66import org.openstreetmap.josm.tools.Utils;
67
68/**
69 * This is a mappaint style that is based on MapCSS rules.
70 */
71public class MapCSSStyleSource extends StyleSource {
72
73 /**
74 * The accepted MIME types sent in the HTTP Accept header.
75 * @since 6867
76 */
77 public static final String MAPCSS_STYLE_MIME_TYPES =
78 "text/x-mapcss, text/mapcss, text/css; q=0.9, text/plain; q=0.8, application/zip, application/octet-stream; q=0.5";
79
80 /**
81 * all rules in this style file
82 */
83 public final List<MapCSSRule> rules = new ArrayList<>();
84 /**
85 * Index of rules in this style file
86 */
87 private final MapCSSStyleIndex ruleIndex = new MapCSSStyleIndex();
88
89 private Color backgroundColorOverride;
90 private String css;
91 private ZipFile zipFile;
92
93 private boolean removeAreaStylePseudoClass;
94
95 /**
96 * This lock prevents concurrent execution of {@link MapCSSRuleIndex#clear() } /
97 * {@link MapCSSRuleIndex#initIndex()} and {@link MapCSSRuleIndex#getRuleCandidates }.
98 *
99 * For efficiency reasons, these methods are synchronized higher up the
100 * stack trace.
101 */
102 public static final ReadWriteLock STYLE_SOURCE_LOCK = new ReentrantReadWriteLock();
103
104 /**
105 * Set of all supported MapCSS keys.
106 */
107 static final Set<String> SUPPORTED_KEYS = new HashSet<>();
108 static {
109 Field[] declaredFields = StyleKeys.class.getDeclaredFields();
110 for (Field f : declaredFields) {
111 try {
112 SUPPORTED_KEYS.add((String) f.get(null));
113 if (!f.getName().toLowerCase(Locale.ENGLISH).replace('_', '-').equals(f.get(null))) {
114 throw new JosmRuntimeException(f.getName());
115 }
116 } catch (IllegalArgumentException | IllegalAccessException ex) {
117 throw new JosmRuntimeException(ex);
118 }
119 }
120 for (LineElement.LineType lt : LineElement.LineType.values()) {
121 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.COLOR);
122 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES);
123 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES_BACKGROUND_COLOR);
124 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES_BACKGROUND_OPACITY);
125 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES_OFFSET);
126 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.LINECAP);
127 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.LINEJOIN);
128 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.MITERLIMIT);
129 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.OFFSET);
130 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.OPACITY);
131 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.REAL_WIDTH);
132 SUPPORTED_KEYS.add(lt.prefix + StyleKeys.WIDTH);
133 }
134 }
135
136 /**
137 * A collection of {@link MapCSSRule}s, that are indexed by tag key and value.
138 *
139 * Speeds up the process of finding all rules that match a certain primitive.
140 *
141 * Rules with a {@link SimpleKeyValueCondition} [key=value] or rules that require a specific key to be set are
142 * indexed. Now you only need to loop the tags of a primitive to retrieve the possibly matching rules.
143 *
144 * To use this index, you need to {@link #add(MapCSSRule)} all rules to it. You then need to call
145 * {@link #initIndex()}. Afterwards, you can use {@link #getRuleCandidates(IPrimitive)} to get an iterator over
146 * all rules that might be applied to that primitive.
147 */
148 public static class MapCSSRuleIndex {
149 /**
150 * This is an iterator over all rules that are marked as possible in the bitset.
151 *
152 * @author Michael Zangl
153 */
154 private final class RuleCandidatesIterator implements Iterator<MapCSSRule>, KeyValueVisitor {
155 private final BitSet ruleCandidates;
156 private int next;
157
158 private RuleCandidatesIterator(BitSet ruleCandidates) {
159 this.ruleCandidates = ruleCandidates;
160 }
161
162 @Override
163 public boolean hasNext() {
164 return next >= 0 && next < rules.size();
165 }
166
167 @Override
168 public MapCSSRule next() {
169 if (!hasNext())
170 throw new NoSuchElementException();
171 MapCSSRule rule = rules.get(next);
172 next = ruleCandidates.nextSetBit(next + 1);
173 return rule;
174 }
175
176 @Override
177 public void remove() {
178 throw new UnsupportedOperationException();
179 }
180
181 @Override
182 public void visitKeyValue(Tagged p, String key, String value) {
183 MapCSSKeyRules v = index.get(key);
184 if (v != null) {
185 BitSet rs = v.get(value);
186 ruleCandidates.or(rs);
187 }
188 }
189
190 /**
191 * Call this before using the iterator.
192 */
193 public void prepare() {
194 next = ruleCandidates.nextSetBit(0);
195 }
196 }
197
198 /**
199 * This is a map of all rules that are only applied if the primitive has a given key (and possibly value)
200 *
201 * @author Michael Zangl
202 */
203 private static final class MapCSSKeyRules {
204 /**
205 * The indexes of rules that might be applied if this tag is present and the value has no special handling.
206 */
207 BitSet generalRules = new BitSet();
208
209 /**
210 * A map that sores the indexes of rules that might be applied if the key=value pair is present on this
211 * primitive. This includes all key=* rules.
212 */
213 Map<String, BitSet> specialRules = new HashMap<>();
214
215 public void addForKey(int ruleIndex) {
216 generalRules.set(ruleIndex);
217 for (BitSet r : specialRules.values()) {
218 r.set(ruleIndex);
219 }
220 }
221
222 public void addForKeyAndValue(String value, int ruleIndex) {
223 BitSet forValue = specialRules.get(value);
224 if (forValue == null) {
225 forValue = new BitSet();
226 forValue.or(generalRules);
227 specialRules.put(value.intern(), forValue);
228 }
229 forValue.set(ruleIndex);
230 }
231
232 public BitSet get(String value) {
233 BitSet forValue = specialRules.get(value);
234 if (forValue != null) return forValue; else return generalRules;
235 }
236 }
237
238 /**
239 * All rules this index is for. Once this index is built, this list is sorted.
240 */
241 private final List<MapCSSRule> rules = new ArrayList<>();
242 /**
243 * All rules that only apply when the given key is present.
244 */
245 private final Map<String, MapCSSKeyRules> index = new HashMap<>();
246 /**
247 * Rules that do not require any key to be present. Only the index in the {@link #rules} array is stored.
248 */
249 private final BitSet remaining = new BitSet();
250
251 /**
252 * Add a rule to this index. This needs to be called before {@link #initIndex()} is called.
253 * @param rule The rule to add.
254 */
255 public void add(MapCSSRule rule) {
256 rules.add(rule);
257 }
258
259 /**
260 * Initialize the index.
261 * <p>
262 * You must own the write lock of STYLE_SOURCE_LOCK when calling this method.
263 */
264 public void initIndex() {
265 Collections.sort(rules);
266 for (int ruleIndex = 0; ruleIndex < rules.size(); ruleIndex++) {
267 MapCSSRule r = rules.get(ruleIndex);
268 for (Selector selector : r.selectors) {
269 Selector selRightmost = selector;
270 while (selRightmost instanceof Selector.ChildOrParentSelector) {
271 selRightmost = ((Selector.ChildOrParentSelector) selRightmost).right;
272 }
273 final List<Condition> conditions = selRightmost.getConditions();
274 if (conditions == null || conditions.isEmpty()) {
275 remaining.set(ruleIndex);
276 continue;
277 }
278 Optional<SimpleKeyValueCondition> lastCondition = Utils.filteredCollection(conditions, SimpleKeyValueCondition.class)
279 .stream()
280 .reduce((first, last) -> last);
281 if (lastCondition.isPresent()) {
282 getEntryInIndex(lastCondition.get().k).addForKeyAndValue(lastCondition.get().v, ruleIndex);
283 } else {
284 String key = findAnyRequiredKey(conditions);
285 if (key != null) {
286 getEntryInIndex(key).addForKey(ruleIndex);
287 } else {
288 remaining.set(ruleIndex);
289 }
290 }
291 }
292 }
293 }
294
295 /**
296 * Search for any key that condition might depend on.
297 *
298 * @param conds The conditions to search through.
299 * @return An arbitrary key this rule depends on or <code>null</code> if there is no such key.
300 */
301 private static String findAnyRequiredKey(List<Condition> conds) {
302 String key = null;
303 for (Condition c : conds) {
304 if (c instanceof KeyCondition) {
305 KeyCondition keyCondition = (KeyCondition) c;
306 if (!keyCondition.negateResult && conditionRequiresKeyPresence(keyCondition.matchType)) {
307 key = keyCondition.label;
308 }
309 } else if (c instanceof KeyValueCondition) {
310 KeyValueCondition keyValueCondition = (KeyValueCondition) c;
311 if (keyValueCondition.requiresExactKeyMatch()) {
312 key = keyValueCondition.k;
313 }
314 }
315 }
316 return key;
317 }
318
319 private static boolean conditionRequiresKeyPresence(KeyMatchType matchType) {
320 return matchType != KeyMatchType.REGEX;
321 }
322
323 private MapCSSKeyRules getEntryInIndex(String key) {
324 MapCSSKeyRules rulesWithMatchingKey = index.get(key);
325 if (rulesWithMatchingKey == null) {
326 rulesWithMatchingKey = new MapCSSKeyRules();
327 index.put(key.intern(), rulesWithMatchingKey);
328 }
329 return rulesWithMatchingKey;
330 }
331
332 /**
333 * Get a subset of all rules that might match the primitive. Rules not included in the result are guaranteed to
334 * not match this primitive.
335 * <p>
336 * You must have a read lock of STYLE_SOURCE_LOCK when calling this method.
337 *
338 * @param osm the primitive to match
339 * @return An iterator over possible rules in the right order.
340 * @since 13810 (signature)
341 */
342 public Iterator<MapCSSRule> getRuleCandidates(IPrimitive osm) {
343 final BitSet ruleCandidates = new BitSet(rules.size());
344 ruleCandidates.or(remaining);
345
346 final RuleCandidatesIterator candidatesIterator = new RuleCandidatesIterator(ruleCandidates);
347 osm.visitKeys(candidatesIterator);
348 candidatesIterator.prepare();
349 return candidatesIterator;
350 }
351
352 /**
353 * Clear the index.
354 * <p>
355 * You must own the write lock STYLE_SOURCE_LOCK when calling this method.
356 */
357 public void clear() {
358 rules.clear();
359 index.clear();
360 remaining.clear();
361 }
362 }
363
364 /**
365 * Constructs a new, active {@link MapCSSStyleSource}.
366 * @param url URL that {@link org.openstreetmap.josm.io.CachedFile} understands
367 * @param name The name for this StyleSource
368 * @param shortdescription The title for that source.
369 */
370 public MapCSSStyleSource(String url, String name, String shortdescription) {
371 super(url, name, shortdescription);
372 }
373
374 /**
375 * Constructs a new {@link MapCSSStyleSource}
376 * @param entry The entry to copy the data (url, name, ...) from.
377 */
378 public MapCSSStyleSource(SourceEntry entry) {
379 super(entry);
380 }
381
382 /**
383 * <p>Creates a new style source from the MapCSS styles supplied in
384 * {@code css}</p>
385 *
386 * @param css the MapCSS style declaration. Must not be null.
387 * @throws IllegalArgumentException if {@code css} is null
388 */
389 public MapCSSStyleSource(String css) {
390 super(null, null, null);
391 CheckParameterUtil.ensureParameterNotNull(css);
392 this.css = css;
393 }
394
395 @Override
396 public void loadStyleSource(boolean metadataOnly) {
397 STYLE_SOURCE_LOCK.writeLock().lock();
398 try {
399 init();
400 rules.clear();
401 ruleIndex.clear();
402 // remove "areaStyle" pseudo classes intended only for validator (causes StackOverflowError otherwise), see #16183
403 removeAreaStylePseudoClass = url == null || !url.contains("validator"); // resource://data/validator/ or xxx.validator.mapcss
404 try (InputStream in = getSourceInputStream()) {
405 try (Reader reader = new BufferedReader(UTFInputStreamReader.create(in))) {
406 // evaluate @media { ... } blocks
407 MapCSSParser preprocessor = new MapCSSParser(reader, MapCSSParser.LexicalState.PREPROCESSOR);
408
409 // do the actual mapcss parsing
410 try (Reader in2 = new StringReader(preprocessor.pp_root(this))) {
411 new MapCSSParser(in2, MapCSSParser.LexicalState.DEFAULT).sheet(this);
412 }
413
414 loadMeta();
415 if (!metadataOnly) {
416 loadCanvas();
417 loadSettings();
418 }
419 } finally {
420 closeSourceInputStream(in);
421 }
422 } catch (IOException | IllegalArgumentException e) {
423 Logging.warn(tr("Failed to load Mappaint styles from ''{0}''. Exception was: {1}", url, e.toString()));
424 Logging.log(Logging.LEVEL_ERROR, e);
425 logError(e);
426 } catch (TokenMgrError e) {
427 Logging.warn(tr("Failed to parse Mappaint styles from ''{0}''. Error was: {1}", url, e.getMessage()));
428 Logging.error(e);
429 logError(e);
430 } catch (ParseException e) {
431 Logging.warn(tr("Failed to parse Mappaint styles from ''{0}''. Error was: {1}", url, e.getMessage()));
432 Logging.error(e);
433 logError(new ParseException(e.getMessage())); // allow e to be garbage collected, it links to the entire token stream
434 }
435 if (metadataOnly) {
436 return;
437 }
438 // optimization: filter rules for different primitive types
439 ruleIndex.buildIndex(rules.stream());
440 loaded = true;
441 } finally {
442 STYLE_SOURCE_LOCK.writeLock().unlock();
443 }
444 }
445
446 @Override
447 public InputStream getSourceInputStream() throws IOException {
448 if (css != null) {
449 return new ByteArrayInputStream(css.getBytes(StandardCharsets.UTF_8));
450 }
451 CachedFile cf = getCachedFile();
452 if (isZip) {
453 File file = cf.getFile();
454 zipFile = new ZipFile(file, StandardCharsets.UTF_8);
455 zipIcons = file;
456 I18n.addTexts(zipIcons);
457 ZipEntry zipEntry = zipFile.getEntry(zipEntryPath);
458 return zipFile.getInputStream(zipEntry);
459 } else {
460 zipFile = null;
461 zipIcons = null;
462 return cf.getInputStream();
463 }
464 }
465
466 @Override
467 @SuppressWarnings("resource")
468 public CachedFile getCachedFile() throws IOException {
469 return new CachedFile(url).setHttpAccept(MAPCSS_STYLE_MIME_TYPES); // NOSONAR
470 }
471
472 @Override
473 public void closeSourceInputStream(InputStream is) {
474 super.closeSourceInputStream(is);
475 if (isZip) {
476 Utils.close(zipFile);
477 }
478 }
479
480 /**
481 * load meta info from a selector "meta"
482 */
483 private void loadMeta() {
484 Cascade c = constructSpecial(Selector.BASE_META);
485 String pTitle = c.get("title", null, String.class);
486 if (title == null) {
487 title = pTitle;
488 }
489 String pIcon = c.get("icon", null, String.class);
490 if (icon == null) {
491 icon = pIcon;
492 }
493 }
494
495 private void loadCanvas() {
496 Cascade c = constructSpecial(Selector.BASE_CANVAS);
497 backgroundColorOverride = c.get("fill-color", null, Color.class);
498 }
499
500 private static void loadSettings(MapCSSRule r, GeneralSelector gs, Environment env) {
501 if (gs.matchesConditions(env)) {
502 env.layer = null;
503 env.layer = gs.getSubpart().getId(env);
504 r.execute(env);
505 }
506 }
507
508 private void loadSettings() {
509 settings.clear();
510 settingValues.clear();
511 settingGroups.clear();
512 MultiCascade mc = new MultiCascade();
513 MultiCascade mcGroups = new MultiCascade();
514 Node n = new Node();
515 n.put("lang", LanguageInfo.getJOSMLocaleCode());
516 // create a fake environment to read the meta data block
517 Environment env = new Environment(n, mc, "default", this);
518 Environment envGroups = new Environment(n, mcGroups, "default", this);
519
520 // Parse rules
521 for (MapCSSRule r : rules) {
522 final Selector gs = r.selectors.get(0);
523 if (gs instanceof GeneralSelector) {
524 if (Selector.BASE_SETTING.equals(gs.getBase())) {
525 loadSettings(r, ((GeneralSelector) gs), env);
526 } else if (Selector.BASE_SETTINGS.equals(gs.getBase())) {
527 loadSettings(r, ((GeneralSelector) gs), envGroups);
528 }
529 }
530 }
531 // Load groups
532 for (Entry<String, Cascade> e : mcGroups.getLayers()) {
533 if ("default".equals(e.getKey())) {
534 Logging.warn("settings requires layer identifier e.g. 'settings::settings_group {...}'");
535 continue;
536 }
537 settingGroups.put(StyleSettingGroup.create(e.getValue(), this, e.getKey()), new ArrayList<>());
538 }
539 // Load settings
540 for (Entry<String, Cascade> e : mc.getLayers()) {
541 if ("default".equals(e.getKey())) {
542 Logging.warn("setting requires layer identifier e.g. 'setting::my_setting {...}'");
543 continue;
544 }
545 Cascade c = e.getValue();
546 StyleSetting set = StyleSettingFactory.create(c, this, e.getKey());
547 if (set != null) {
548 settings.add(set);
549 settingValues.put(e.getKey(), set.getValue());
550 String groupId = c.get("group", null, String.class);
551 if (groupId != null) {
552 final StyleSettingGroup group = settingGroups.keySet().stream()
553 .filter(g -> g.key.equals(groupId))
554 .findAny()
555 .orElseThrow(() -> new IllegalArgumentException("Unknown settings group: " + groupId));
556 settingGroups.get(group).add(set);
557 }
558 }
559 }
560 settings.sort(null);
561 }
562
563 private Cascade constructSpecial(String type) {
564
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 final boolean matches = r.selectors.stream().anyMatch(gs -> gs instanceof GeneralSelector
574 && gs.getBase().equals(type)
575 && ((GeneralSelector) gs).matchesConditions(env));
576 if (matches) {
577 r.execute(env);
578 }
579 }
580 return mc.getCascade("default");
581 }
582
583 @Override
584 public Color getBackgroundColorOverride() {
585 return backgroundColorOverride;
586 }
587
588 @Override
589 public void apply(MultiCascade mc, IPrimitive osm, double scale, boolean pretendWayIsClosed) {
590
591 Environment env = new Environment(osm, mc, null, this);
592 // the declaration indices are sorted, so it suffices to save the last used index
593 int lastDeclUsed = -1;
594
595 Iterator<MapCSSRule> candidates = ruleIndex.getRuleCandidates(osm);
596 while (candidates.hasNext()) {
597 MapCSSRule r = candidates.next();
598 for (Selector s : r.selectors) {
599 env.clearSelectorMatchingInformation();
600 env.layer = s.getSubpart().getId(env);
601 String sub = env.layer;
602 if (!s.matches(env)) { // as side effect env.parent will be set (if s is a child selector)
603 continue;
604 }
605 if (s.getRange().contains(scale)) {
606 mc.range = Range.cut(mc.range, s.getRange());
607 } else {
608 mc.range = mc.range.reduceAround(scale, s.getRange());
609 continue;
610 }
611
612 if (r.declaration.idx == lastDeclUsed)
613 continue; // don't apply one declaration more than once
614 lastDeclUsed = r.declaration.idx;
615 if ("*".equals(sub)) {
616 for (Entry<String, Cascade> entry : mc.getLayers()) {
617 env.layer = entry.getKey();
618 if ("*".equals(env.layer)) {
619 continue;
620 }
621 r.execute(env);
622 }
623 }
624 env.layer = sub;
625 r.execute(env);
626 }
627 }
628 }
629
630 /**
631 * Evaluate a supports condition
632 * @param feature The feature to evaluate for
633 * @param val The additional parameter passed to evaluate
634 * @return <code>true</code> if JSOM supports that feature
635 */
636 public boolean evalSupportsDeclCondition(String feature, Object val) {
637 if (feature == null) return false;
638 if (SUPPORTED_KEYS.contains(feature)) return true;
639 switch (feature) {
640 case "user-agent":
641 String s = Cascade.convertTo(val, String.class);
642 return "josm".equals(s);
643 case "min-josm-version":
644 Float min = Cascade.convertTo(val, Float.class);
645 return min != null && Math.round(min) <= Version.getInstance().getVersion();
646 case "max-josm-version":
647 Float max = Cascade.convertTo(val, Float.class);
648 return max != null && Math.round(max) >= Version.getInstance().getVersion();
649 default:
650 return false;
651 }
652 }
653
654 /**
655 * Removes "meta" rules. Not needed for validator.
656 * @since 13633
657 */
658 public void removeMetaRules() {
659 rules.removeIf(x -> x.selectors.get(0) instanceof GeneralSelector && Selector.BASE_META.equals(x.selectors.get(0).getBase()));
660 }
661
662 /**
663 * Whether to remove "areaStyle" pseudo classes. Only for use in MapCSSParser!
664 * @return whether to remove "areaStyle" pseudo classes
665 */
666 public boolean isRemoveAreaStylePseudoClass() {
667 return removeAreaStylePseudoClass;
668 }
669
670 @Override
671 public String toString() {
672 return rules.stream().map(MapCSSRule::toString).collect(Collectors.joining("\n"));
673 }
674}
Note: See TracBrowser for help on using the repository browser.