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}