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