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.StyleSource;
052import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.KeyCondition;
053import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.KeyMatchType;
054import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.KeyValueCondition;
055import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.PseudoClassCondition;
056import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.SimpleKeyValueCondition;
057import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.AbstractSelector;
058import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.ChildOrParentSelector;
059import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector;
060import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.OptimizedGeneralSelector;
061import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser;
062import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException;
063import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.TokenMgrError;
064import org.openstreetmap.josm.gui.mappaint.styleelement.LineElement;
065import org.openstreetmap.josm.io.CachedFile;
066import org.openstreetmap.josm.io.UTFInputStreamReader;
067import org.openstreetmap.josm.tools.CheckParameterUtil;
068import org.openstreetmap.josm.tools.I18n;
069import org.openstreetmap.josm.tools.JosmRuntimeException;
070import org.openstreetmap.josm.tools.LanguageInfo;
071import org.openstreetmap.josm.tools.Logging;
072import org.openstreetmap.josm.tools.Utils;
073
074/**
075 * This is a mappaint style that is based on MapCSS rules.
076 */
077public class MapCSSStyleSource extends StyleSource {
078
079    /**
080     * The accepted MIME types sent in the HTTP Accept header.
081     * @since 6867
082     */
083    public static final String MAPCSS_STYLE_MIME_TYPES =
084            "text/x-mapcss, text/mapcss, text/css; q=0.9, text/plain; q=0.8, application/zip, application/octet-stream; q=0.5";
085
086    /**
087     * all rules in this style file
088     */
089    public final List<MapCSSRule> rules = new ArrayList<>();
090    /**
091     * Rules for nodes
092     */
093    public final MapCSSRuleIndex nodeRules = new MapCSSRuleIndex();
094    /**
095     * Rules for ways without tag area=no
096     */
097    public final MapCSSRuleIndex wayRules = new MapCSSRuleIndex();
098    /**
099     * Rules for ways with tag area=no
100     */
101    public final MapCSSRuleIndex wayNoAreaRules = new MapCSSRuleIndex();
102    /**
103     * Rules for relations that are not multipolygon relations
104     */
105    public final MapCSSRuleIndex relationRules = new MapCSSRuleIndex();
106    /**
107     * Rules for multipolygon relations
108     */
109    public final MapCSSRuleIndex multipolygonRules = new MapCSSRuleIndex();
110    /**
111     * rules to apply canvas properties
112     */
113    public final MapCSSRuleIndex canvasRules = new MapCSSRuleIndex();
114
115    private Color backgroundColorOverride;
116    private String css;
117    private ZipFile zipFile;
118
119    /**
120     * This lock prevents concurrent execution of {@link MapCSSRuleIndex#clear() } /
121     * {@link MapCSSRuleIndex#initIndex()} and {@link MapCSSRuleIndex#getRuleCandidates }.
122     *
123     * For efficiency reasons, these methods are synchronized higher up the
124     * stack trace.
125     */
126    public static final ReadWriteLock STYLE_SOURCE_LOCK = new ReentrantReadWriteLock();
127
128    /**
129     * Set of all supported MapCSS keys.
130     */
131    static final Set<String> SUPPORTED_KEYS = new HashSet<>();
132    static {
133        Field[] declaredFields = StyleKeys.class.getDeclaredFields();
134        for (Field f : declaredFields) {
135            try {
136                SUPPORTED_KEYS.add((String) f.get(null));
137                if (!f.getName().toLowerCase(Locale.ENGLISH).replace('_', '-').equals(f.get(null))) {
138                    throw new JosmRuntimeException(f.getName());
139                }
140            } catch (IllegalArgumentException | IllegalAccessException ex) {
141                throw new JosmRuntimeException(ex);
142            }
143        }
144        for (LineElement.LineType lt : LineElement.LineType.values()) {
145            SUPPORTED_KEYS.add(lt.prefix + StyleKeys.COLOR);
146            SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES);
147            SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES_BACKGROUND_COLOR);
148            SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES_BACKGROUND_OPACITY);
149            SUPPORTED_KEYS.add(lt.prefix + StyleKeys.DASHES_OFFSET);
150            SUPPORTED_KEYS.add(lt.prefix + StyleKeys.LINECAP);
151            SUPPORTED_KEYS.add(lt.prefix + StyleKeys.LINEJOIN);
152            SUPPORTED_KEYS.add(lt.prefix + StyleKeys.MITERLIMIT);
153            SUPPORTED_KEYS.add(lt.prefix + StyleKeys.OFFSET);
154            SUPPORTED_KEYS.add(lt.prefix + StyleKeys.OPACITY);
155            SUPPORTED_KEYS.add(lt.prefix + StyleKeys.REAL_WIDTH);
156            SUPPORTED_KEYS.add(lt.prefix + StyleKeys.WIDTH);
157        }
158    }
159
160    /**
161     * A collection of {@link MapCSSRule}s, that are indexed by tag key and value.
162     *
163     * Speeds up the process of finding all rules that match a certain primitive.
164     *
165     * Rules with a {@link SimpleKeyValueCondition} [key=value] or rules that require a specific key to be set are
166     * indexed. Now you only need to loop the tags of a primitive to retrieve the possibly matching rules.
167     *
168     * To use this index, you need to {@link #add(MapCSSRule)} all rules to it. You then need to call
169     * {@link #initIndex()}. Afterwards, you can use {@link #getRuleCandidates(IPrimitive)} to get an iterator over
170     * all rules that might be applied to that primitive.
171     */
172    public static class MapCSSRuleIndex {
173        /**
174         * This is an iterator over all rules that are marked as possible in the bitset.
175         *
176         * @author Michael Zangl
177         */
178        private final class RuleCandidatesIterator implements Iterator<MapCSSRule>, KeyValueVisitor {
179            private final BitSet ruleCandidates;
180            private int next;
181
182            private RuleCandidatesIterator(BitSet ruleCandidates) {
183                this.ruleCandidates = ruleCandidates;
184            }
185
186            @Override
187            public boolean hasNext() {
188                return next >= 0 && next < rules.size();
189            }
190
191            @Override
192            public MapCSSRule next() {
193                if (!hasNext())
194                    throw new NoSuchElementException();
195                MapCSSRule rule = rules.get(next);
196                next = ruleCandidates.nextSetBit(next + 1);
197                return rule;
198            }
199
200            @Override
201            public void remove() {
202                throw new UnsupportedOperationException();
203            }
204
205            @Override
206            public void visitKeyValue(Tagged p, String key, String value) {
207                MapCSSKeyRules v = index.get(key);
208                if (v != null) {
209                    BitSet rs = v.get(value);
210                    ruleCandidates.or(rs);
211                }
212            }
213
214            /**
215             * Call this before using the iterator.
216             */
217            public void prepare() {
218                next = ruleCandidates.nextSetBit(0);
219            }
220        }
221
222        /**
223         * This is a map of all rules that are only applied if the primitive has a given key (and possibly value)
224         *
225         * @author Michael Zangl
226         */
227        private static final class MapCSSKeyRules {
228            /**
229             * The indexes of rules that might be applied if this tag is present and the value has no special handling.
230             */
231            BitSet generalRules = new BitSet();
232
233            /**
234             * A map that sores the indexes of rules that might be applied if the key=value pair is present on this
235             * primitive. This includes all key=* rules.
236             */
237            Map<String, BitSet> specialRules = new HashMap<>();
238
239            public void addForKey(int ruleIndex) {
240                generalRules.set(ruleIndex);
241                for (BitSet r : specialRules.values()) {
242                    r.set(ruleIndex);
243                }
244            }
245
246            public void addForKeyAndValue(String value, int ruleIndex) {
247                BitSet forValue = specialRules.get(value);
248                if (forValue == null) {
249                    forValue = new BitSet();
250                    forValue.or(generalRules);
251                    specialRules.put(value.intern(), forValue);
252                }
253                forValue.set(ruleIndex);
254            }
255
256            public BitSet get(String value) {
257                BitSet forValue = specialRules.get(value);
258                if (forValue != null) return forValue; else return generalRules;
259            }
260        }
261
262        /**
263         * All rules this index is for. Once this index is built, this list is sorted.
264         */
265        private final List<MapCSSRule> rules = new ArrayList<>();
266        /**
267         * All rules that only apply when the given key is present.
268         */
269        private final Map<String, MapCSSKeyRules> index = new HashMap<>();
270        /**
271         * Rules that do not require any key to be present. Only the index in the {@link #rules} array is stored.
272         */
273        private final BitSet remaining = new BitSet();
274
275        /**
276         * Add a rule to this index. This needs to be called before {@link #initIndex()} is called.
277         * @param rule The rule to add.
278         */
279        public void add(MapCSSRule rule) {
280            rules.add(rule);
281        }
282
283        /**
284         * Initialize the index.
285         * <p>
286         * You must own the write lock of STYLE_SOURCE_LOCK when calling this method.
287         */
288        public void initIndex() {
289            Collections.sort(rules);
290            for (int ruleIndex = 0; ruleIndex < rules.size(); ruleIndex++) {
291                MapCSSRule r = rules.get(ruleIndex);
292                // find the rightmost selector, this must be a GeneralSelector
293                Selector selRightmost = r.selector;
294                while (selRightmost instanceof ChildOrParentSelector) {
295                    selRightmost = ((ChildOrParentSelector) selRightmost).right;
296                }
297                OptimizedGeneralSelector s = (OptimizedGeneralSelector) selRightmost;
298                if (s.conds == null) {
299                    remaining.set(ruleIndex);
300                    continue;
301                }
302                List<SimpleKeyValueCondition> sk = new ArrayList<>(Utils.filteredCollection(s.conds,
303                        SimpleKeyValueCondition.class));
304                if (!sk.isEmpty()) {
305                    SimpleKeyValueCondition c = sk.get(sk.size() - 1);
306                    getEntryInIndex(c.k).addForKeyAndValue(c.v, ruleIndex);
307                } else {
308                    String key = findAnyRequiredKey(s.conds);
309                    if (key != null) {
310                        getEntryInIndex(key).addForKey(ruleIndex);
311                    } else {
312                        remaining.set(ruleIndex);
313                    }
314                }
315            }
316        }
317
318        /**
319         * Search for any key that condition might depend on.
320         *
321         * @param conds The conditions to search through.
322         * @return An arbitrary key this rule depends on or <code>null</code> if there is no such key.
323         */
324        private static String findAnyRequiredKey(List<Condition> conds) {
325            String key = null;
326            for (Condition c : conds) {
327                if (c instanceof KeyCondition) {
328                    KeyCondition keyCondition = (KeyCondition) c;
329                    if (!keyCondition.negateResult && conditionRequiresKeyPresence(keyCondition.matchType)) {
330                        key = keyCondition.label;
331                    }
332                } else if (c instanceof KeyValueCondition) {
333                    KeyValueCondition keyValueCondition = (KeyValueCondition) c;
334                    if (keyValueCondition.requiresExactKeyMatch()) {
335                        key = keyValueCondition.k;
336                    }
337                }
338            }
339            return key;
340        }
341
342        private static boolean conditionRequiresKeyPresence(KeyMatchType matchType) {
343            return matchType != KeyMatchType.REGEX;
344        }
345
346        private MapCSSKeyRules getEntryInIndex(String key) {
347            MapCSSKeyRules rulesWithMatchingKey = index.get(key);
348            if (rulesWithMatchingKey == null) {
349                rulesWithMatchingKey = new MapCSSKeyRules();
350                index.put(key.intern(), rulesWithMatchingKey);
351            }
352            return rulesWithMatchingKey;
353        }
354
355        /**
356         * Get a subset of all rules that might match the primitive. Rules not included in the result are guaranteed to
357         * not match this primitive.
358         * <p>
359         * You must have a read lock of STYLE_SOURCE_LOCK when calling this method.
360         *
361         * @param osm the primitive to match
362         * @return An iterator over possible rules in the right order.
363         * @since 13810 (signature)
364         */
365        public Iterator<MapCSSRule> getRuleCandidates(IPrimitive osm) {
366            final BitSet ruleCandidates = new BitSet(rules.size());
367            ruleCandidates.or(remaining);
368
369            final RuleCandidatesIterator candidatesIterator = new RuleCandidatesIterator(ruleCandidates);
370            osm.visitKeys(candidatesIterator);
371            candidatesIterator.prepare();
372            return candidatesIterator;
373        }
374
375        /**
376         * Clear the index.
377         * <p>
378         * You must own the write lock STYLE_SOURCE_LOCK when calling this method.
379         */
380        public void clear() {
381            rules.clear();
382            index.clear();
383            remaining.clear();
384        }
385    }
386
387    /**
388     * Constructs a new, active {@link MapCSSStyleSource}.
389     * @param url URL that {@link org.openstreetmap.josm.io.CachedFile} understands
390     * @param name The name for this StyleSource
391     * @param shortdescription The title for that source.
392     */
393    public MapCSSStyleSource(String url, String name, String shortdescription) {
394        super(url, name, shortdescription);
395    }
396
397    /**
398     * Constructs a new {@link MapCSSStyleSource}
399     * @param entry The entry to copy the data (url, name, ...) from.
400     */
401    public MapCSSStyleSource(SourceEntry entry) {
402        super(entry);
403    }
404
405    /**
406     * <p>Creates a new style source from the MapCSS styles supplied in
407     * {@code css}</p>
408     *
409     * @param css the MapCSS style declaration. Must not be null.
410     * @throws IllegalArgumentException if {@code css} is null
411     */
412    public MapCSSStyleSource(String css) {
413        super(null, null, null);
414        CheckParameterUtil.ensureParameterNotNull(css);
415        this.css = css;
416    }
417
418    @Override
419    public void loadStyleSource(boolean metadataOnly) {
420        STYLE_SOURCE_LOCK.writeLock().lock();
421        try {
422            init();
423            rules.clear();
424            nodeRules.clear();
425            wayRules.clear();
426            wayNoAreaRules.clear();
427            relationRules.clear();
428            multipolygonRules.clear();
429            canvasRules.clear();
430            try (InputStream in = getSourceInputStream()) {
431                try (Reader reader = new BufferedReader(UTFInputStreamReader.create(in))) {
432                    // evaluate @media { ... } blocks
433                    MapCSSParser preprocessor = new MapCSSParser(reader, MapCSSParser.LexicalState.PREPROCESSOR);
434                    String mapcss = preprocessor.pp_root(this);
435
436                    // do the actual mapcss parsing
437                    Reader in2 = new StringReader(mapcss);
438                    MapCSSParser parser = new MapCSSParser(in2, MapCSSParser.LexicalState.DEFAULT);
439                    parser.sheet(this);
440
441                    loadMeta();
442                    if (!metadataOnly) {
443                        loadCanvas();
444                        loadSettings();
445                    }
446                    // remove "areaStyle" pseudo classes intended only for validator (causes StackOverflowError otherwise)
447                    removeAreaStyleClasses();
448                } finally {
449                    closeSourceInputStream(in);
450                }
451            } catch (IOException | IllegalArgumentException e) {
452                Logging.warn(tr("Failed to load Mappaint styles from ''{0}''. Exception was: {1}", url, e.toString()));
453                Logging.log(Logging.LEVEL_ERROR, e);
454                logError(e);
455            } catch (TokenMgrError e) {
456                Logging.warn(tr("Failed to parse Mappaint styles from ''{0}''. Error was: {1}", url, e.getMessage()));
457                Logging.error(e);
458                logError(e);
459            } catch (ParseException e) {
460                Logging.warn(tr("Failed to parse Mappaint styles from ''{0}''. Error was: {1}", url, e.getMessage()));
461                Logging.error(e);
462                logError(new ParseException(e.getMessage())); // allow e to be garbage collected, it links to the entire token stream
463            }
464            if (metadataOnly) {
465                return;
466            }
467            // optimization: filter rules for different primitive types
468            for (MapCSSRule r: rules) {
469                // find the rightmost selector, this must be a GeneralSelector
470                Selector selRightmost = r.selector;
471                while (selRightmost instanceof ChildOrParentSelector) {
472                    selRightmost = ((ChildOrParentSelector) selRightmost).right;
473                }
474                MapCSSRule optRule = new MapCSSRule(r.selector.optimizedBaseCheck(), r.declaration);
475                final String base = ((GeneralSelector) selRightmost).getBase();
476                switch (base) {
477                    case Selector.BASE_NODE:
478                        nodeRules.add(optRule);
479                        break;
480                    case Selector.BASE_WAY:
481                        wayNoAreaRules.add(optRule);
482                        wayRules.add(optRule);
483                        break;
484                    case Selector.BASE_AREA:
485                        wayRules.add(optRule);
486                        multipolygonRules.add(optRule);
487                        break;
488                    case Selector.BASE_RELATION:
489                        relationRules.add(optRule);
490                        multipolygonRules.add(optRule);
491                        break;
492                    case Selector.BASE_ANY:
493                        nodeRules.add(optRule);
494                        wayRules.add(optRule);
495                        wayNoAreaRules.add(optRule);
496                        relationRules.add(optRule);
497                        multipolygonRules.add(optRule);
498                        break;
499                    case Selector.BASE_CANVAS:
500                        canvasRules.add(r);
501                        break;
502                    case Selector.BASE_META:
503                    case Selector.BASE_SETTING:
504                        break;
505                    default:
506                        final RuntimeException e = new JosmRuntimeException(MessageFormat.format("Unknown MapCSS base selector {0}", base));
507                        Logging.warn(tr("Failed to parse Mappaint styles from ''{0}''. Error was: {1}", url, e.getMessage()));
508                        Logging.error(e);
509                        logError(e);
510                }
511            }
512            nodeRules.initIndex();
513            wayRules.initIndex();
514            wayNoAreaRules.initIndex();
515            relationRules.initIndex();
516            multipolygonRules.initIndex();
517            canvasRules.initIndex();
518            loaded = true;
519        } finally {
520            STYLE_SOURCE_LOCK.writeLock().unlock();
521        }
522    }
523
524    @Override
525    public InputStream getSourceInputStream() throws IOException {
526        if (css != null) {
527            return new ByteArrayInputStream(css.getBytes(StandardCharsets.UTF_8));
528        }
529        CachedFile cf = getCachedFile();
530        if (isZip) {
531            File file = cf.getFile();
532            zipFile = new ZipFile(file, StandardCharsets.UTF_8);
533            zipIcons = file;
534            I18n.addTexts(zipIcons);
535            ZipEntry zipEntry = zipFile.getEntry(zipEntryPath);
536            return zipFile.getInputStream(zipEntry);
537        } else {
538            zipFile = null;
539            zipIcons = null;
540            return cf.getInputStream();
541        }
542    }
543
544    @Override
545    @SuppressWarnings("resource")
546    public CachedFile getCachedFile() throws IOException {
547        return new CachedFile(url).setHttpAccept(MAPCSS_STYLE_MIME_TYPES); // NOSONAR
548    }
549
550    @Override
551    public void closeSourceInputStream(InputStream is) {
552        super.closeSourceInputStream(is);
553        if (isZip) {
554            Utils.close(zipFile);
555        }
556    }
557
558    /**
559     * load meta info from a selector "meta"
560     */
561    private void loadMeta() {
562        Cascade c = constructSpecial(Selector.BASE_META);
563        String pTitle = c.get("title", null, String.class);
564        if (title == null) {
565            title = pTitle;
566        }
567        String pIcon = c.get("icon", null, String.class);
568        if (icon == null) {
569            icon = pIcon;
570        }
571    }
572
573    private void loadCanvas() {
574        Cascade c = constructSpecial(Selector.BASE_CANVAS);
575        backgroundColorOverride = c.get("fill-color", null, Color.class);
576    }
577
578    private void loadSettings() {
579        settings.clear();
580        settingValues.clear();
581        MultiCascade mc = new MultiCascade();
582        Node n = new Node();
583        String code = LanguageInfo.getJOSMLocaleCode();
584        n.put("lang", code);
585        // create a fake environment to read the meta data block
586        Environment env = new Environment(n, mc, "default", this);
587
588        for (MapCSSRule r : rules) {
589            if (r.selector instanceof GeneralSelector) {
590                GeneralSelector gs = (GeneralSelector) r.selector;
591                if (Selector.BASE_SETTING.equals(gs.getBase())) {
592                    if (!gs.matchesConditions(env)) {
593                        continue;
594                    }
595                    env.layer = null;
596                    env.layer = gs.getSubpart().getId(env);
597                    r.execute(env);
598                }
599            }
600        }
601        for (Entry<String, Cascade> e : mc.getLayers()) {
602            if ("default".equals(e.getKey())) {
603                Logging.warn("setting requires layer identifier e.g. 'setting::my_setting {...}'");
604                continue;
605            }
606            Cascade c = e.getValue();
607            String type = c.get("type", null, String.class);
608            StyleSetting set = null;
609            if ("boolean".equals(type)) {
610                set = BooleanStyleSetting.create(c, this, e.getKey());
611            } else {
612                Logging.warn("Unknown setting type: "+type);
613            }
614            if (set != null) {
615                settings.add(set);
616                settingValues.put(e.getKey(), set.getValue());
617            }
618        }
619        settings.sort(null);
620    }
621
622    private Cascade constructSpecial(String type) {
623
624        MultiCascade mc = new MultiCascade();
625        Node n = new Node();
626        String code = LanguageInfo.getJOSMLocaleCode();
627        n.put("lang", code);
628        // create a fake environment to read the meta data block
629        Environment env = new Environment(n, mc, "default", this);
630
631        for (MapCSSRule r : rules) {
632            if (r.selector instanceof GeneralSelector) {
633                GeneralSelector gs = (GeneralSelector) r.selector;
634                if (gs.getBase().equals(type)) {
635                    if (!gs.matchesConditions(env)) {
636                        continue;
637                    }
638                    r.execute(env);
639                }
640            }
641        }
642        return mc.getCascade("default");
643    }
644
645    @Override
646    public Color getBackgroundColorOverride() {
647        return backgroundColorOverride;
648    }
649
650    @Override
651    public void apply(MultiCascade mc, IPrimitive osm, double scale, boolean pretendWayIsClosed) {
652        MapCSSRuleIndex matchingRuleIndex;
653        if (osm instanceof INode) {
654            matchingRuleIndex = nodeRules;
655        } else if (osm instanceof IWay) {
656            if (OsmUtils.isFalse(osm.get("area"))) {
657                matchingRuleIndex = wayNoAreaRules;
658            } else {
659                matchingRuleIndex = wayRules;
660            }
661        } else if (osm instanceof IRelation) {
662            if (((IRelation<?>) osm).isMultipolygon()) {
663                matchingRuleIndex = multipolygonRules;
664            } else if (osm.hasKey("#canvas")) {
665                matchingRuleIndex = canvasRules;
666            } else {
667                matchingRuleIndex = relationRules;
668            }
669        } else {
670            throw new IllegalArgumentException("Unsupported type: " + osm);
671        }
672
673        Environment env = new Environment(osm, mc, null, this);
674        // the declaration indices are sorted, so it suffices to save the last used index
675        int lastDeclUsed = -1;
676
677        Iterator<MapCSSRule> candidates = matchingRuleIndex.getRuleCandidates(osm);
678        while (candidates.hasNext()) {
679            MapCSSRule r = candidates.next();
680            env.clearSelectorMatchingInformation();
681            env.layer = r.selector.getSubpart().getId(env);
682            String sub = env.layer;
683            if (r.selector.matches(env)) { // as side effect env.parent will be set (if s is a child selector)
684                Selector s = r.selector;
685                if (s.getRange().contains(scale)) {
686                    mc.range = Range.cut(mc.range, s.getRange());
687                } else {
688                    mc.range = mc.range.reduceAround(scale, s.getRange());
689                    continue;
690                }
691
692                if (r.declaration.idx == lastDeclUsed)
693                    continue; // don't apply one declaration more than once
694                lastDeclUsed = r.declaration.idx;
695                if ("*".equals(sub)) {
696                    for (Entry<String, Cascade> entry : mc.getLayers()) {
697                        env.layer = entry.getKey();
698                        if ("*".equals(env.layer)) {
699                            continue;
700                        }
701                        r.execute(env);
702                    }
703                }
704                env.layer = sub;
705                r.execute(env);
706            }
707        }
708    }
709
710    /**
711     * Evaluate a supports condition
712     * @param feature The feature to evaluate for
713     * @param val The additional parameter passed to evaluate
714     * @return <code>true</code> if JSOM supports that feature
715     */
716    public boolean evalSupportsDeclCondition(String feature, Object val) {
717        if (feature == null) return false;
718        if (SUPPORTED_KEYS.contains(feature)) return true;
719        switch (feature) {
720            case "user-agent":
721                String s = Cascade.convertTo(val, String.class);
722                return "josm".equals(s);
723            case "min-josm-version":
724                Float min = Cascade.convertTo(val, Float.class);
725                return min != null && Math.round(min) <= Version.getInstance().getVersion();
726            case "max-josm-version":
727                Float max = Cascade.convertTo(val, Float.class);
728                return max != null && Math.round(max) >= Version.getInstance().getVersion();
729            default:
730                return false;
731        }
732    }
733
734    /**
735     * Removes "meta" rules. Not needed for validator.
736     * @since 13633
737     */
738    public void removeMetaRules() {
739        for (Iterator<MapCSSRule> it = rules.iterator(); it.hasNext();) {
740            MapCSSRule x = it.next();
741            if (x.selector instanceof GeneralSelector) {
742                GeneralSelector gs = (GeneralSelector) x.selector;
743                if (Selector.BASE_META.equals(gs.base)) {
744                    it.remove();
745                }
746            }
747        }
748    }
749
750    /**
751     * Removes "areaStyle" pseudo-classes. Only needed for validator.
752     * @since 13633
753     */
754    public void removeAreaStyleClasses() {
755        for (Iterator<MapCSSRule> it = rules.iterator(); it.hasNext();) {
756            removeAreaStyleClasses(it.next().selector);
757        }
758    }
759
760    private static void removeAreaStyleClasses(Selector sel) {
761        if (sel instanceof ChildOrParentSelector) {
762            removeAreaStyleClasses((ChildOrParentSelector) sel);
763        } else if (sel instanceof AbstractSelector) {
764            removeAreaStyleClasses((AbstractSelector) sel);
765        }
766    }
767
768    private static void removeAreaStyleClasses(ChildOrParentSelector sel) {
769        removeAreaStyleClasses(sel.left);
770        removeAreaStyleClasses(sel.right);
771    }
772
773    private static void removeAreaStyleClasses(AbstractSelector sel) {
774        if (sel.conds != null) {
775            for (Iterator<Condition> it = sel.conds.iterator(); it.hasNext();) {
776                Condition c = it.next();
777                if (c instanceof PseudoClassCondition) {
778                    PseudoClassCondition cc = (PseudoClassCondition) c;
779                    if ("areaStyle".equals(cc.method.getName())) {
780                        Logging.warn("Removing 'areaStyle' pseudo-class from "+sel+". This class is only meant for validator");
781                        it.remove();
782                    }
783                }
784            }
785        }
786    }
787
788    @Override
789    public String toString() {
790        return Utils.join("\n", rules);
791    }
792}