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