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