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}