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