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