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