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