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

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

see #18802 - MapCSSRule: support list of selectors

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