001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.mappaint.mapcss;
003
004import static org.openstreetmap.josm.data.projection.Ellipsoid.WGS84;
005
006import java.text.MessageFormat;
007import java.util.ArrayList;
008import java.util.Collection;
009import java.util.Collections;
010import java.util.LinkedHashSet;
011import java.util.List;
012import java.util.Objects;
013import java.util.function.IntFunction;
014import java.util.function.IntSupplier;
015import java.util.regex.PatternSyntaxException;
016import java.util.stream.Collectors;
017
018import org.openstreetmap.josm.data.osm.INode;
019import org.openstreetmap.josm.data.osm.IPrimitive;
020import org.openstreetmap.josm.data.osm.IRelation;
021import org.openstreetmap.josm.data.osm.IRelationMember;
022import org.openstreetmap.josm.data.osm.IWay;
023import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
024import org.openstreetmap.josm.data.osm.OsmUtils;
025import org.openstreetmap.josm.data.osm.Relation;
026import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
027import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
028import org.openstreetmap.josm.gui.mappaint.Environment;
029import org.openstreetmap.josm.gui.mappaint.Range;
030import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.OpenEndPseudoClassCondition;
031import org.openstreetmap.josm.tools.CheckParameterUtil;
032import org.openstreetmap.josm.tools.Geometry;
033import org.openstreetmap.josm.tools.Logging;
034import org.openstreetmap.josm.tools.Pair;
035import org.openstreetmap.josm.tools.Utils;
036
037/**
038 * MapCSS selector.
039 *
040 * A rule has two parts, a selector and a declaration block
041 * e.g.
042 * <pre>
043 * way[highway=residential]
044 * { width: 10; color: blue; }
045 * </pre>
046 *
047 * The selector decides, if the declaration block gets applied or not.
048 *
049 * All implementing classes of Selector are immutable.
050 */
051public interface Selector {
052
053    /** selector base that matches anything. */
054    String BASE_ANY = "*";
055
056    /** selector base that matches on OSM object node. */
057    String BASE_NODE = "node";
058
059    /** selector base that matches on OSM object way. */
060    String BASE_WAY = "way";
061
062    /** selector base that matches on OSM object relation. */
063    String BASE_RELATION = "relation";
064
065    /** selector base that matches with any area regardless of whether the area border is only modelled with a single way or with
066     * a set of ways glued together with a relation.*/
067    String BASE_AREA = "area";
068
069    /** selector base for special rules containing meta information. */
070    String BASE_META = "meta";
071
072    /** selector base for style information not specific to nodes, ways or relations. */
073    String BASE_CANVAS = "canvas";
074
075    /** selector base for artificial bases created to use preferences. */
076    String BASE_SETTING = "setting";
077
078    /** selector base for grouping settings. */
079    String BASE_SETTINGS = "settings";
080
081    /**
082     * Apply the selector to the primitive and check if it matches.
083     *
084     * @param env the Environment. env.mc and env.layer are read-only when matching a selector.
085     * env.source is not needed. This method will set the matchingReferrers field of env as
086     * a side effect! Make sure to clear it before invoking this method.
087     * @return true, if the selector applies
088     */
089    boolean matches(Environment env);
090
091    /**
092     * Returns the subpart, if supported. A subpart identifies different rendering layers (<code>::subpart</code> syntax).
093     * @return the subpart, if supported
094     * @throws UnsupportedOperationException if not supported
095     */
096    Subpart getSubpart();
097
098    /**
099     * Returns the scale range, an interval of the form "lower &lt; x &lt;= upper" where 0 &lt;= lower &lt; upper.
100     * @return the scale range, if supported
101     * @throws UnsupportedOperationException if not supported
102     */
103    Range getRange();
104
105    /**
106     * Create an "optimized" copy of this selector that omits the base check.
107     *
108     * For the style source, the list of rules is preprocessed, such that
109     * there is a separate list of rules for nodes, ways, ...
110     *
111     * This means that the base check does not have to be performed
112     * for each rule, but only once for each primitive.
113     *
114     * @return a selector that is identical to this object, except the base of the
115     * "rightmost" selector is not checked
116     */
117    Selector optimizedBaseCheck();
118
119    /**
120     * The type of child of parent selector.
121     * @see ChildOrParentSelector
122     */
123    enum ChildOrParentSelectorType {
124        CHILD, PARENT, SUBSET_OR_EQUAL, NOT_SUBSET_OR_EQUAL, SUPERSET_OR_EQUAL, NOT_SUPERSET_OR_EQUAL, CROSSING, SIBLING,
125    }
126
127    /**
128     * <p>Represents a child selector or a parent selector.</p>
129     *
130     * <p>In addition to the standard CSS notation for child selectors, JOSM also supports
131     * an "inverse" notation:</p>
132     * <pre>
133     *    selector_a &gt; selector_b { ... }       // the standard notation (child selector)
134     *    relation[type=route] &gt; way { ... }    // example (all ways of a route)
135     *
136     *    selector_a &lt; selector_b { ... }       // the inverse notation (parent selector)
137     *    node[traffic_calming] &lt; way { ... }   // example (way that has a traffic calming node)
138     * </pre>
139     * <p>Child: see <a href="https://josm.openstreetmap.de/wiki/Help/Styles/MapCSSImplementation#Childselector">wiki</a>
140     * <br>Parent: see <a href="https://josm.openstreetmap.de/wiki/Help/Styles/MapCSSImplementation#Parentselector">wiki</a></p>
141     */
142    class ChildOrParentSelector implements Selector {
143        public final Selector left;
144        public final LinkSelector link;
145        public final Selector right;
146        public final ChildOrParentSelectorType type;
147
148        /**
149         * Constructs a new {@code ChildOrParentSelector}.
150         * @param a the first selector
151         * @param link link
152         * @param b the second selector
153         * @param type the selector type
154         */
155        public ChildOrParentSelector(Selector a, LinkSelector link, Selector b, ChildOrParentSelectorType type) {
156            CheckParameterUtil.ensureParameterNotNull(a, "a");
157            CheckParameterUtil.ensureParameterNotNull(b, "b");
158            CheckParameterUtil.ensureParameterNotNull(link, "link");
159            CheckParameterUtil.ensureParameterNotNull(type, "type");
160            this.left = a;
161            this.link = link;
162            this.right = b;
163            this.type = type;
164        }
165
166        /**
167         * <p>Finds the first referrer matching {@link #left}</p>
168         *
169         * <p>The visitor works on an environment and it saves the matching
170         * referrer in {@code e.parent} and its relative position in the
171         * list referrers "child list" in {@code e.index}.</p>
172         *
173         * <p>If after execution {@code e.parent} is null, no matching
174         * referrer was found.</p>
175         *
176         */
177        private class MatchingReferrerFinder implements PrimitiveVisitor {
178            private final Environment e;
179
180            /**
181             * Constructor
182             * @param e the environment against which we match
183             */
184            MatchingReferrerFinder(Environment e) {
185                this.e = e;
186            }
187
188            @Override
189            public void visit(INode n) {
190                // node should never be a referrer
191                throw new AssertionError();
192            }
193
194            private <T extends IPrimitive> void doVisit(T parent, IntSupplier counter, IntFunction<IPrimitive> getter) {
195                // If e.parent is already set to the first matching referrer.
196                // We skip any following referrer injected into the visitor.
197                if (e.parent != null) return;
198
199                if (!left.matches(e.withPrimitive(parent)))
200                    return;
201                int count = counter.getAsInt();
202                if (link.conds == null) {
203                    // index is not needed, we can avoid the sequential search below
204                    e.parent = parent;
205                    e.count = count;
206                    return;
207                }
208                for (int i = 0; i < count; i++) {
209                    if (getter.apply(i).equals(e.osm) && link.matches(e.withParentAndIndexAndLinkContext(parent, i, count))) {
210                        e.parent = parent;
211                        e.index = i;
212                        e.count = count;
213                        return;
214                    }
215                }
216            }
217
218            @Override
219            public void visit(IWay<?> w) {
220                doVisit(w, w::getNodesCount, w::getNode);
221            }
222
223            @Override
224            public void visit(IRelation<?> r) {
225                doVisit(r, r::getMembersCount, i -> r.getMember(i).getMember());
226            }
227        }
228
229        private abstract static class AbstractFinder implements PrimitiveVisitor {
230            protected final Environment e;
231
232            protected AbstractFinder(Environment e) {
233                this.e = e;
234            }
235
236            @Override
237            public void visit(INode n) {
238            }
239
240            @Override
241            public void visit(IWay<?> w) {
242            }
243
244            @Override
245            public void visit(IRelation<?> r) {
246            }
247
248            public void visit(Collection<? extends IPrimitive> primitives) {
249                for (IPrimitive p : primitives) {
250                    if (e.child != null) {
251                        // abort if first match has been found
252                        break;
253                    } else if (isPrimitiveUsable(p)) {
254                        p.accept(this);
255                    }
256                }
257            }
258
259            public boolean isPrimitiveUsable(IPrimitive p) {
260                return !e.osm.equals(p) && p.isUsable();
261            }
262
263            protected void addToChildren(Environment e, IPrimitive p) {
264                if (e.children == null) {
265                    e.children = new LinkedHashSet<>();
266                }
267                e.children.add(p);
268            }
269        }
270
271        private class MultipolygonOpenEndFinder extends AbstractFinder {
272
273            @Override
274            public void visit(IWay<?> w) {
275                w.visitReferrers(innerVisitor);
276            }
277
278            MultipolygonOpenEndFinder(Environment e) {
279                super(e);
280            }
281
282            private final PrimitiveVisitor innerVisitor = new AbstractFinder(e) {
283                @Override
284                public void visit(IRelation<?> r) {
285                    if (r instanceof Relation && left.matches(e.withPrimitive(r))) {
286                        final List<?> openEnds = MultipolygonCache.getInstance().get((Relation) r).getOpenEnds();
287                        final int openEndIndex = openEnds.indexOf(e.osm);
288                        if (openEndIndex >= 0) {
289                            e.parent = r;
290                            e.index = openEndIndex;
291                            e.count = openEnds.size();
292                        }
293                    }
294                }
295            };
296        }
297
298        private final class CrossingFinder extends AbstractFinder {
299
300            private final String layer;
301
302            private CrossingFinder(Environment e) {
303                super(e);
304                CheckParameterUtil.ensureThat(e.osm instanceof IWay, "Only ways are supported");
305                layer = OsmUtils.getLayer(e.osm);
306            }
307
308            @Override
309            public void visit(IWay<?> w) {
310                if (Objects.equals(layer, OsmUtils.getLayer(w))
311                    && left.matches(new Environment(w).withParent(e.osm))
312                    && e.osm instanceof IWay && Geometry.PolygonIntersection.CROSSING.equals(
313                            Geometry.polygonIntersection(w.getNodes(), ((IWay<?>) e.osm).getNodes()))) {
314                    addToChildren(e, w);
315                }
316            }
317        }
318
319        /**
320         * Finds elements which are inside the right element, collects those in {@code children}
321         */
322        private class ContainsFinder extends AbstractFinder {
323            protected List<IPrimitive> toCheck;
324
325            protected ContainsFinder(Environment e) {
326                super(e);
327                CheckParameterUtil.ensureThat(!(e.osm instanceof INode), "Nodes not supported");
328            }
329
330            @Override
331            public void visit(Collection<? extends IPrimitive> primitives) {
332                for (IPrimitive p : primitives) {
333                    if (p != e.osm && isPrimitiveUsable(p) && left.matches(new Environment(p).withParent(e.osm))) {
334                        if (toCheck == null) {
335                            toCheck = new ArrayList<>();
336                        }
337                        toCheck.add(p);
338                    }
339                }
340            }
341
342            void execGeometryTests() {
343                if (toCheck == null || toCheck.isEmpty())
344                    return;
345                for (IPrimitive p : Geometry.filterInsideAnyPolygon(toCheck, e.osm)) {
346                    addToChildren(e, p);
347                }
348            }
349        }
350
351        /**
352         * Finds elements which are inside the left element, or in other words, it finds elements enclosing e.osm.
353         * The found enclosing elements are collected in {@code e.children}.
354         */
355        private class InsideOrEqualFinder extends AbstractFinder {
356
357            protected InsideOrEqualFinder(Environment e) {
358                super(e);
359            }
360
361            @Override
362            public void visit(IWay<?> w) {
363                if (left.matches(new Environment(w).withParent(e.osm))
364                        && w.getBBox().bounds(e.osm.getBBox())
365                        && !Geometry.filterInsidePolygon(Collections.singletonList(e.osm), w).isEmpty()) {
366                    addToChildren(e, w);
367                }
368            }
369
370            @Override
371            public void visit(IRelation<?> r) {
372                if (r instanceof Relation && r.isMultipolygon() && r.getBBox().bounds(e.osm.getBBox())
373                        && left.matches(new Environment(r).withParent(e.osm))
374                        && !Geometry.filterInsideMultipolygon(Collections.singletonList(e.osm), (Relation) r).isEmpty()) {
375                    addToChildren(e, r);
376                }
377            }
378        }
379
380        private void visitBBox(Environment e, AbstractFinder finder) {
381            boolean withNodes = finder instanceof ContainsFinder;
382            if (left instanceof OptimizedGeneralSelector) {
383                if (withNodes && ((OptimizedGeneralSelector) left).matchesBase(OsmPrimitiveType.NODE)) {
384                    finder.visit(e.osm.getDataSet().searchNodes(e.osm.getBBox()));
385                }
386                if (((OptimizedGeneralSelector) left).matchesBase(OsmPrimitiveType.WAY)) {
387                    finder.visit(e.osm.getDataSet().searchWays(e.osm.getBBox()));
388                }
389                if (((OptimizedGeneralSelector) left).matchesBase(OsmPrimitiveType.RELATION)) {
390                    finder.visit(e.osm.getDataSet().searchRelations(e.osm.getBBox()));
391                }
392            } else {
393                if (withNodes) {
394                    finder.visit(e.osm.getDataSet().searchNodes(e.osm.getBBox()));
395                }
396                finder.visit(e.osm.getDataSet().searchWays(e.osm.getBBox()));
397                finder.visit(e.osm.getDataSet().searchRelations(e.osm.getBBox()));
398            }
399        }
400
401        private static boolean isArea(IPrimitive p) {
402            return (p instanceof IWay && ((IWay<?>) p).isClosed() && ((IWay<?>) p).getNodesCount() >= 4)
403                    || (p instanceof IRelation && p.isMultipolygon() && !p.isIncomplete());
404        }
405
406        @Override
407        public boolean matches(Environment e) {
408
409            if (!right.matches(e))
410                return false;
411
412            if (ChildOrParentSelectorType.SUBSET_OR_EQUAL == type || ChildOrParentSelectorType.NOT_SUBSET_OR_EQUAL == type) {
413
414                if (e.osm.getDataSet() == null || !isArea(e.osm)) {
415                    // only areas can contain elements
416                    return ChildOrParentSelectorType.NOT_SUBSET_OR_EQUAL == type;
417                }
418                ContainsFinder containsFinder = new ContainsFinder(e);
419                e.parent = e.osm;
420
421                visitBBox(e, containsFinder);
422                containsFinder.execGeometryTests();
423                return ChildOrParentSelectorType.SUBSET_OR_EQUAL == type ? e.children != null : e.children == null;
424
425            } else if (ChildOrParentSelectorType.SUPERSET_OR_EQUAL == type || ChildOrParentSelectorType.NOT_SUPERSET_OR_EQUAL == type) {
426
427                if (e.osm.getDataSet() == null || (e.osm instanceof INode && ((INode) e.osm).getCoor() == null)
428                        || (!(e.osm instanceof INode) && !isArea(e.osm))) {
429                    return ChildOrParentSelectorType.NOT_SUPERSET_OR_EQUAL == type;
430                }
431
432                InsideOrEqualFinder insideOrEqualFinder = new InsideOrEqualFinder(e);
433                e.parent = e.osm;
434
435                visitBBox(e, insideOrEqualFinder);
436                return ChildOrParentSelectorType.SUPERSET_OR_EQUAL == type ? e.children != null : e.children == null;
437
438            } else if (ChildOrParentSelectorType.CROSSING == type && e.osm instanceof IWay) {
439                e.parent = e.osm;
440                if (right instanceof OptimizedGeneralSelector
441                        && ((OptimizedGeneralSelector) right).matchesBase(OsmPrimitiveType.WAY)) {
442                    final CrossingFinder crossingFinder = new CrossingFinder(e);
443                    crossingFinder.visit(e.osm.getDataSet().searchWays(e.osm.getBBox()));
444                }
445                return e.children != null;
446            } else if (ChildOrParentSelectorType.SIBLING == type) {
447                if (e.osm instanceof INode) {
448                    for (IPrimitive ref : e.osm.getReferrers(true)) {
449                        if (ref instanceof IWay) {
450                            IWay<?> w = (IWay<?>) ref;
451                            final int i = w.getNodes().indexOf(e.osm);
452                            if (i - 1 >= 0) {
453                                final INode n = w.getNode(i - 1);
454                                final Environment e2 = e.withPrimitive(n).withParent(w).withChild(e.osm);
455                                if (left.matches(e2) && link.matches(e2.withLinkContext())) {
456                                    e.child = n;
457                                    e.index = i;
458                                    e.count = w.getNodesCount();
459                                    e.parent = w;
460                                    return true;
461                                }
462                            }
463                        }
464                    }
465                }
466            } else if (ChildOrParentSelectorType.CHILD == type
467                    && link.conds != null && !link.conds.isEmpty()
468                    && link.conds.get(0) instanceof OpenEndPseudoClassCondition) {
469                if (e.osm instanceof INode) {
470                    e.osm.visitReferrers(new MultipolygonOpenEndFinder(e));
471                    return e.parent != null;
472                }
473            } else if (ChildOrParentSelectorType.CHILD == type) {
474                MatchingReferrerFinder collector = new MatchingReferrerFinder(e);
475                e.osm.visitReferrers(collector);
476                if (e.parent != null)
477                    return true;
478            } else if (ChildOrParentSelectorType.PARENT == type) {
479                if (e.osm instanceof IWay) {
480                    List<? extends INode> wayNodes = ((IWay<?>) e.osm).getNodes();
481                    for (int i = 0; i < wayNodes.size(); i++) {
482                        INode n = wayNodes.get(i);
483                        if (left.matches(e.withPrimitive(n))
484                            && link.matches(e.withChildAndIndexAndLinkContext(n, i, wayNodes.size()))) {
485                            e.child = n;
486                            e.index = i;
487                            e.count = wayNodes.size();
488                            return true;
489                        }
490                    }
491                } else if (e.osm instanceof IRelation) {
492                    List<? extends IRelationMember<?>> members = ((IRelation<?>) e.osm).getMembers();
493                    for (int i = 0; i < members.size(); i++) {
494                        IPrimitive member = members.get(i).getMember();
495                        if (left.matches(e.withPrimitive(member))
496                            && link.matches(e.withChildAndIndexAndLinkContext(member, i, members.size()))) {
497                            e.child = member;
498                            e.index = i;
499                            e.count = members.size();
500                            return true;
501                        }
502                    }
503                }
504            }
505            return false;
506        }
507
508        @Override
509        public Subpart getSubpart() {
510            return right.getSubpart();
511        }
512
513        @Override
514        public Range getRange() {
515            return right.getRange();
516        }
517
518        @Override
519        public Selector optimizedBaseCheck() {
520            return new ChildOrParentSelector(left, link, right.optimizedBaseCheck(), type);
521        }
522
523        @Override
524        public String toString() {
525            return left.toString() + ' ' + (ChildOrParentSelectorType.PARENT == type ? '<' : '>') + link + ' ' + right;
526        }
527    }
528
529    /**
530     * Super class of {@link org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector} and
531     * {@link org.openstreetmap.josm.gui.mappaint.mapcss.Selector.LinkSelector}.
532     * @since 5841
533     */
534    abstract class AbstractSelector implements Selector {
535
536        protected final List<Condition> conds;
537
538        protected AbstractSelector(List<Condition> conditions) {
539            if (conditions == null || conditions.isEmpty()) {
540                this.conds = null;
541            } else {
542                this.conds = conditions;
543            }
544        }
545
546        /**
547         * Determines if all conditions match the given environment.
548         * @param env The environment to check
549         * @return {@code true} if all conditions apply, false otherwise.
550         */
551        @Override
552        public boolean matches(Environment env) {
553            CheckParameterUtil.ensureParameterNotNull(env, "env");
554            if (conds == null) return true;
555            for (Condition c : conds) {
556                try {
557                    if (!c.applies(env)) return false;
558                } catch (PatternSyntaxException e) {
559                    Logging.log(Logging.LEVEL_ERROR, "PatternSyntaxException while applying condition" + c + ':', e);
560                    return false;
561                }
562            }
563            return true;
564        }
565
566        /**
567         * Returns the list of conditions.
568         * @return the list of conditions
569         */
570        public List<Condition> getConditions() {
571            if (conds == null) {
572                return Collections.emptyList();
573            }
574            return Collections.unmodifiableList(conds);
575        }
576    }
577
578    /**
579     * In a child selector, conditions on the link between a parent and a child object.
580     * See <a href="https://josm.openstreetmap.de/wiki/Help/Styles/MapCSSImplementation#Linkselector">wiki</a>
581     */
582    class LinkSelector extends AbstractSelector {
583
584        public LinkSelector(List<Condition> conditions) {
585            super(conditions);
586        }
587
588        @Override
589        public boolean matches(Environment env) {
590            Utils.ensure(env.isLinkContext(), "Requires LINK context in environment, got ''{0}''", env.getContext());
591            return super.matches(env);
592        }
593
594        @Override
595        public Subpart getSubpart() {
596            throw new UnsupportedOperationException("Not supported yet.");
597        }
598
599        @Override
600        public Range getRange() {
601            throw new UnsupportedOperationException("Not supported yet.");
602        }
603
604        @Override
605        public Selector optimizedBaseCheck() {
606            throw new UnsupportedOperationException();
607        }
608
609        @Override
610        public String toString() {
611            return "LinkSelector{conditions=" + conds + '}';
612        }
613    }
614
615    /**
616     * General selector. See <a href="https://josm.openstreetmap.de/wiki/Help/Styles/MapCSSImplementation#Selectors">wiki</a>
617     */
618    class GeneralSelector extends OptimizedGeneralSelector {
619
620        public GeneralSelector(String base, Pair<Integer, Integer> zoom, List<Condition> conds, Subpart subpart) {
621            super(base, zoom, conds, subpart);
622        }
623
624        public boolean matchesConditions(Environment e) {
625            return super.matches(e);
626        }
627
628        @Override
629        public Selector optimizedBaseCheck() {
630            return new OptimizedGeneralSelector(this);
631        }
632
633        @Override
634        public boolean matches(Environment e) {
635            return matchesBase(e) && super.matches(e);
636        }
637    }
638
639    /**
640     * Superclass of {@link GeneralSelector}. Used to create an "optimized" copy of this selector that omits the base check.
641     * @see Selector#optimizedBaseCheck
642     */
643    class OptimizedGeneralSelector extends AbstractSelector {
644        public final String base;
645        public final Range range;
646        public final Subpart subpart;
647
648        public OptimizedGeneralSelector(String base, Pair<Integer, Integer> zoom, List<Condition> conds, Subpart subpart) {
649            super(conds);
650            this.base = checkBase(base);
651            if (zoom != null) {
652                int a = zoom.a == null ? 0 : zoom.a;
653                int b = zoom.b == null ? Integer.MAX_VALUE : zoom.b;
654                if (a <= b) {
655                    range = fromLevel(a, b);
656                } else {
657                    range = Range.ZERO_TO_INFINITY;
658                }
659            } else {
660                range = Range.ZERO_TO_INFINITY;
661            }
662            this.subpart = subpart != null ? subpart : Subpart.DEFAULT_SUBPART;
663        }
664
665        public OptimizedGeneralSelector(String base, Range range, List<Condition> conds, Subpart subpart) {
666            super(conds);
667            this.base = checkBase(base);
668            this.range = range;
669            this.subpart = subpart != null ? subpart : Subpart.DEFAULT_SUBPART;
670        }
671
672        public OptimizedGeneralSelector(GeneralSelector s) {
673            this(s.base, s.range, s.conds, s.subpart);
674        }
675
676        @Override
677        public Subpart getSubpart() {
678            return subpart;
679        }
680
681        @Override
682        public Range getRange() {
683            return range;
684        }
685
686        /**
687         * Set base and check if this is a known value.
688         * @param base value for base
689         * @return the matching String constant for a known value
690         * @throws IllegalArgumentException if value is not knwon
691         */
692        private static String checkBase(String base) {
693            switch(base) {
694            case "*": return BASE_ANY;
695            case "node": return BASE_NODE;
696            case "way": return BASE_WAY;
697            case "relation": return BASE_RELATION;
698            case "area": return BASE_AREA;
699            case "meta": return BASE_META;
700            case "canvas": return BASE_CANVAS;
701            case "setting": return BASE_SETTING;
702            case "settings": return BASE_SETTINGS;
703            default:
704                throw new IllegalArgumentException(MessageFormat.format("Unknown MapCSS base selector {0}", base));
705            }
706        }
707
708        public String getBase() {
709            return base;
710        }
711
712        public boolean matchesBase(OsmPrimitiveType type) {
713            if (BASE_ANY.equals(base)) {
714                return true;
715            } else if (OsmPrimitiveType.NODE == type) {
716                return BASE_NODE.equals(base);
717            } else if (OsmPrimitiveType.WAY == type) {
718                return BASE_WAY.equals(base) || BASE_AREA.equals(base);
719            } else if (OsmPrimitiveType.RELATION == type) {
720                return BASE_AREA.equals(base) || BASE_RELATION.equals(base) || BASE_CANVAS.equals(base);
721            }
722            return false;
723        }
724
725        public boolean matchesBase(IPrimitive p) {
726            if (!matchesBase(p.getType())) {
727                return false;
728            } else {
729                if (p instanceof IRelation) {
730                    if (BASE_AREA.equals(base)) {
731                        return ((IRelation<?>) p).isMultipolygon();
732                    } else if (BASE_CANVAS.equals(base)) {
733                        return p.get("#canvas") != null;
734                    }
735                }
736                return true;
737            }
738        }
739
740        public boolean matchesBase(Environment e) {
741            return matchesBase(e.osm);
742        }
743
744        @Override
745        public Selector optimizedBaseCheck() {
746            throw new UnsupportedOperationException();
747        }
748
749        public static Range fromLevel(int a, int b) {
750            if (a > b)
751                throw new AssertionError();
752            double lower = 0;
753            double upper = Double.POSITIVE_INFINITY;
754            if (b != Integer.MAX_VALUE) {
755                lower = level2scale(b + 1);
756            }
757            if (a != 0) {
758                upper = level2scale(a);
759            }
760            return new Range(lower, upper);
761        }
762
763        public static double level2scale(int lvl) {
764            if (lvl < 0)
765                throw new IllegalArgumentException("lvl must be >= 0 but is "+lvl);
766            // preliminary formula - map such that mapnik imagery tiles of the same
767            // or similar level are displayed at the given scale
768            return 2.0 * Math.PI * WGS84.a / Math.pow(2.0, lvl) / 2.56;
769        }
770
771        public static int scale2level(double scale) {
772            if (scale < 0)
773                throw new IllegalArgumentException("scale must be >= 0 but is "+scale);
774            return (int) Math.floor(Math.log(2 * Math.PI * WGS84.a / 2.56 / scale) / Math.log(2));
775        }
776
777        @Override
778        public String toString() {
779            return base
780                    + (Range.ZERO_TO_INFINITY.equals(range) ? "" : range)
781                    + (conds != null ? conds.stream().map(String::valueOf).collect(Collectors.joining("")) : "")
782                    + (subpart != null && subpart != Subpart.DEFAULT_SUBPART ? ("::" + subpart) : "");
783        }
784    }
785}