// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.mappaint.mapcss; import static org.openstreetmap.josm.data.projection.Ellipsoid.WGS84; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.NoSuchElementException; import java.util.Set; import java.util.function.IntFunction; import java.util.function.IntSupplier; import java.util.regex.PatternSyntaxException; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.osm.Node; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.OsmPrimitiveType; import org.openstreetmap.josm.data.osm.Relation; import org.openstreetmap.josm.data.osm.RelationMember; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor; import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache; import org.openstreetmap.josm.gui.mappaint.Environment; import org.openstreetmap.josm.gui.mappaint.Range; import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.OpenEndPseudoClassCondition; import org.openstreetmap.josm.tools.CheckParameterUtil; import org.openstreetmap.josm.tools.Geometry; import org.openstreetmap.josm.tools.Pair; import org.openstreetmap.josm.tools.SubclassFilteredCollection; import org.openstreetmap.josm.tools.Utils; /** * MapCSS selector. * * A rule has two parts, a selector and a declaration block * e.g. * <pre> * way[highway=residential] * { width: 10; color: blue; } * </pre> * * The selector decides, if the declaration block gets applied or not. * * All implementing classes of Selector are immutable. */ public interface Selector { /** * Apply the selector to the primitive and check if it matches. * * @param env the Environment. env.mc and env.layer are read-only when matching a selector. * env.source is not needed. This method will set the matchingReferrers field of env as * a side effect! Make sure to clear it before invoking this method. * @return true, if the selector applies */ boolean matches(Environment env); /** * Returns the subpart, if supported. A subpart identifies different rendering layers (<code>::subpart</code> syntax). * @return the subpart, if supported * @throws UnsupportedOperationException if not supported */ Subpart getSubpart(); /** * Returns the scale range, an interval of the form "lower < x <= upper" where 0 <= lower < upper. * @return the scale range, if supported * @throws UnsupportedOperationException if not supported */ Range getRange(); /** * Create an "optimized" copy of this selector that omits the base check. * * For the style source, the list of rules is preprocessed, such that * there is a separate list of rules for nodes, ways, ... * * This means that the base check does not have to be performed * for each rule, but only once for each primitive. * * @return a selector that is identical to this object, except the base of the * "rightmost" selector is not checked */ Selector optimizedBaseCheck(); /** * The type of child of parent selector. * @see ChildOrParentSelector */ enum ChildOrParentSelectorType { CHILD, PARENT, ELEMENT_OF, CROSSING, SIBLING } /** * <p>Represents a child selector or a parent selector.</p> * * <p>In addition to the standard CSS notation for child selectors, JOSM also supports * an "inverse" notation:</p> * <pre> * selector_a > selector_b { ... } // the standard notation (child selector) * relation[type=route] > way { ... } // example (all ways of a route) * * selector_a < selector_b { ... } // the inverse notation (parent selector) * node[traffic_calming] < way { ... } // example (way that has a traffic calming node) * </pre> * <p>Child: see <a href="https://josm.openstreetmap.de/wiki/Help/Styles/MapCSSImplementation#Childselector">wiki</a> * <br>Parent: see <a href="https://josm.openstreetmap.de/wiki/Help/Styles/MapCSSImplementation#Parentselector">wiki</a></p> */ class ChildOrParentSelector implements Selector { public final Selector left; public final LinkSelector link; public final Selector right; public final ChildOrParentSelectorType type; /** * Constructs a new {@code ChildOrParentSelector}. * @param a the first selector * @param link link * @param b the second selector * @param type the selector type */ public ChildOrParentSelector(Selector a, LinkSelector link, Selector b, ChildOrParentSelectorType type) { CheckParameterUtil.ensureParameterNotNull(a, "a"); CheckParameterUtil.ensureParameterNotNull(b, "b"); CheckParameterUtil.ensureParameterNotNull(link, "link"); CheckParameterUtil.ensureParameterNotNull(type, "type"); this.left = a; this.link = link; this.right = b; this.type = type; } /** * <p>Finds the first referrer matching {@link #left}</p> * * <p>The visitor works on an environment and it saves the matching * referrer in {@code e.parent} and its relative position in the * list referrers "child list" in {@code e.index}.</p> * * <p>If after execution {@code e.parent} is null, no matching * referrer was found.</p> * */ private class MatchingReferrerFinder extends AbstractVisitor { private final Environment e; /** * Constructor * @param e the environment against which we match */ MatchingReferrerFinder(Environment e) { this.e = e; } @Override public void visit(Node n) { // node should never be a referrer throw new AssertionError(); } private <T extends OsmPrimitive> void doVisit(T parent, IntSupplier counter, IntFunction<OsmPrimitive> getter) { // If e.parent is already set to the first matching referrer. // We skip any following referrer injected into the visitor. if (e.parent != null) return; if (!left.matches(e.withPrimitive(parent))) return; int count = counter.getAsInt(); if (link.conds == null) { // index is not needed, we can avoid the sequential search below e.parent = parent; e.count = count; return; } for (int i = 0; i < count; i++) { if (getter.apply(i).equals(e.osm) && link.matches(e.withParentAndIndexAndLinkContext(parent, i, count))) { e.parent = parent; e.index = i; e.count = count; return; } } } @Override public void visit(Way w) { doVisit(w, w::getNodesCount, w::getNode); } @Override public void visit(Relation r) { doVisit(r, r::getMembersCount, i -> r.getMember(i).getMember()); } } private abstract static class AbstractFinder extends AbstractVisitor { protected final Environment e; protected AbstractFinder(Environment e) { this.e = e; } @Override public void visit(Node n) { } @Override public void visit(Way w) { } @Override public void visit(Relation r) { } public void visit(Collection<? extends OsmPrimitive> primitives) { for (OsmPrimitive p : primitives) { if (e.child != null) { // abort if first match has been found break; } else if (isPrimitiveUsable(p)) { p.accept(this); } } } public boolean isPrimitiveUsable(OsmPrimitive p) { return !e.osm.equals(p) && p.isUsable(); } } private class MultipolygonOpenEndFinder extends AbstractFinder { @Override public void visit(Way w) { w.visitReferrers(innerVisitor); } MultipolygonOpenEndFinder(Environment e) { super(e); } private final AbstractVisitor innerVisitor = new AbstractFinder(e) { @Override public void visit(Relation r) { if (left.matches(e.withPrimitive(r))) { final List<Node> openEnds = MultipolygonCache.getInstance().get(r).getOpenEnds(); final int openEndIndex = openEnds.indexOf(e.osm); if (openEndIndex >= 0) { e.parent = r; e.index = openEndIndex; e.count = openEnds.size(); } } } }; } private final class CrossingFinder extends AbstractFinder { private CrossingFinder(Environment e) { super(e); CheckParameterUtil.ensureThat(e.osm instanceof Way, "Only ways are supported"); } @Override public void visit(Way w) { if (e.child == null && left.matches(new Environment(w).withParent(e.osm))) { if (e.osm instanceof Way && Geometry.PolygonIntersection.CROSSING.equals( Geometry.polygonIntersection(w.getNodes(), ((Way) e.osm).getNodes()))) { e.child = w; } } } } private class ContainsFinder extends AbstractFinder { protected ContainsFinder(Environment e) { super(e); CheckParameterUtil.ensureThat(!(e.osm instanceof Node), "Nodes not supported"); } @Override public void visit(Node n) { if (e.child == null && left.matches(new Environment(n).withParent(e.osm))) { if ((e.osm instanceof Way && Geometry.nodeInsidePolygon(n, ((Way) e.osm).getNodes())) || (e.osm instanceof Relation && ( (Relation) e.osm).isMultipolygon() && Geometry.isNodeInsideMultiPolygon(n, (Relation) e.osm, null))) { e.child = n; } } } @Override public void visit(Way w) { if (e.child == null && left.matches(new Environment(w).withParent(e.osm))) { if ((e.osm instanceof Way && Geometry.PolygonIntersection.FIRST_INSIDE_SECOND.equals( Geometry.polygonIntersection(w.getNodes(), ((Way) e.osm).getNodes()))) || (e.osm instanceof Relation && ( (Relation) e.osm).isMultipolygon() && Geometry.isPolygonInsideMultiPolygon(w.getNodes(), (Relation) e.osm, null))) { e.child = w; } } } } @Override public boolean matches(Environment e) { if (!right.matches(e)) return false; if (ChildOrParentSelectorType.ELEMENT_OF.equals(type)) { if (e.osm instanceof Node || e.osm.getDataSet() == null) { // nodes cannot contain elements return false; } ContainsFinder containsFinder; try { // if right selector also matches relations and if matched primitive is a way which is part of a multipolygon, // use the multipolygon for further analysis if (!(e.osm instanceof Way) || (right instanceof OptimizedGeneralSelector && !((OptimizedGeneralSelector) right).matchesBase(OsmPrimitiveType.RELATION))) { throw new NoSuchElementException(); } final Collection<Relation> multipolygons = Utils.filteredCollection(SubclassFilteredCollection.filter( e.osm.getReferrers(), p -> p.hasTag("type", "multipolygon")), Relation.class); final Relation multipolygon = multipolygons.iterator().next(); if (multipolygon == null) throw new NoSuchElementException(); final Set<OsmPrimitive> members = multipolygon.getMemberPrimitives(); containsFinder = new ContainsFinder(new Environment(multipolygon)) { @Override public boolean isPrimitiveUsable(OsmPrimitive p) { return super.isPrimitiveUsable(p) && !members.contains(p); } }; } catch (NoSuchElementException ignore) { Main.trace(ignore); containsFinder = new ContainsFinder(e); } e.parent = e.osm; if (left instanceof OptimizedGeneralSelector) { if (((OptimizedGeneralSelector) left).matchesBase(OsmPrimitiveType.NODE)) { containsFinder.visit(e.osm.getDataSet().searchNodes(e.osm.getBBox())); } if (((OptimizedGeneralSelector) left).matchesBase(OsmPrimitiveType.WAY)) { containsFinder.visit(e.osm.getDataSet().searchWays(e.osm.getBBox())); } } else { // use slow test containsFinder.visit(e.osm.getDataSet().allPrimitives()); } return e.child != null; } else if (ChildOrParentSelectorType.CROSSING.equals(type) && e.osm instanceof Way) { e.parent = e.osm; final CrossingFinder crossingFinder = new CrossingFinder(e); if (right instanceof OptimizedGeneralSelector && ((OptimizedGeneralSelector) right).matchesBase(OsmPrimitiveType.WAY)) { crossingFinder.visit(e.osm.getDataSet().searchWays(e.osm.getBBox())); } return e.child != null; } else if (ChildOrParentSelectorType.SIBLING.equals(type)) { if (e.osm instanceof Node) { for (Way w : Utils.filteredCollection(e.osm.getReferrers(true), Way.class)) { final int i = w.getNodes().indexOf(e.osm); if (i - 1 >= 0) { final Node n = w.getNode(i - 1); final Environment e2 = e.withPrimitive(n).withParent(w).withChild(e.osm); if (left.matches(e2) && link.matches(e2.withLinkContext())) { e.child = n; e.index = i; e.count = w.getNodesCount(); e.parent = w; return true; } } } } } else if (ChildOrParentSelectorType.CHILD.equals(type) && link.conds != null && !link.conds.isEmpty() && link.conds.get(0) instanceof OpenEndPseudoClassCondition) { if (e.osm instanceof Node) { e.osm.visitReferrers(new MultipolygonOpenEndFinder(e)); return e.parent != null; } } else if (ChildOrParentSelectorType.CHILD.equals(type)) { MatchingReferrerFinder collector = new MatchingReferrerFinder(e); e.osm.visitReferrers(collector); if (e.parent != null) return true; } else if (ChildOrParentSelectorType.PARENT.equals(type)) { if (e.osm instanceof Way) { List<Node> wayNodes = ((Way) e.osm).getNodes(); for (int i = 0; i < wayNodes.size(); i++) { Node n = wayNodes.get(i); if (left.matches(e.withPrimitive(n))) { if (link.matches(e.withChildAndIndexAndLinkContext(n, i, wayNodes.size()))) { e.child = n; e.index = i; e.count = wayNodes.size(); return true; } } } } else if (e.osm instanceof Relation) { List<RelationMember> members = ((Relation) e.osm).getMembers(); for (int i = 0; i < members.size(); i++) { OsmPrimitive member = members.get(i).getMember(); if (left.matches(e.withPrimitive(member))) { if (link.matches(e.withChildAndIndexAndLinkContext(member, i, members.size()))) { e.child = member; e.index = i; e.count = members.size(); return true; } } } } } return false; } @Override public Subpart getSubpart() { return right.getSubpart(); } @Override public Range getRange() { return right.getRange(); } @Override public Selector optimizedBaseCheck() { return new ChildOrParentSelector(left, link, right.optimizedBaseCheck(), type); } @Override public String toString() { return left.toString() + ' ' + (ChildOrParentSelectorType.PARENT.equals(type) ? '<' : '>') + link + ' ' + right; } } /** * Super class of {@link org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector} and * {@link org.openstreetmap.josm.gui.mappaint.mapcss.Selector.LinkSelector}. * @since 5841 */ abstract class AbstractSelector implements Selector { protected final List<Condition> conds; protected AbstractSelector(List<Condition> conditions) { if (conditions == null || conditions.isEmpty()) { this.conds = null; } else { this.conds = conditions; } } /** * Determines if all conditions match the given environment. * @param env The environment to check * @return {@code true} if all conditions apply, false otherwise. */ @Override public boolean matches(Environment env) { CheckParameterUtil.ensureParameterNotNull(env, "env"); if (conds == null) return true; for (Condition c : conds) { try { if (!c.applies(env)) return false; } catch (PatternSyntaxException e) { Main.error(e, "PatternSyntaxException while applying condition" + c + ':'); return false; } } return true; } /** * Returns the list of conditions. * @return the list of conditions */ public List<Condition> getConditions() { if (conds == null) { return Collections.emptyList(); } return Collections.unmodifiableList(conds); } } /** * In a child selector, conditions on the link between a parent and a child object. * See <a href="https://josm.openstreetmap.de/wiki/Help/Styles/MapCSSImplementation#Linkselector">wiki</a> */ class LinkSelector extends AbstractSelector { public LinkSelector(List<Condition> conditions) { super(conditions); } @Override public boolean matches(Environment env) { Utils.ensure(env.isLinkContext(), "Requires LINK context in environment, got ''{0}''", env.getContext()); return super.matches(env); } @Override public Subpart getSubpart() { throw new UnsupportedOperationException("Not supported yet."); } @Override public Range getRange() { throw new UnsupportedOperationException("Not supported yet."); } @Override public Selector optimizedBaseCheck() { throw new UnsupportedOperationException(); } @Override public String toString() { return "LinkSelector{conditions=" + conds + '}'; } } /** * General selector. See <a href="https://josm.openstreetmap.de/wiki/Help/Styles/MapCSSImplementation#Selectors">wiki</a> */ class GeneralSelector extends OptimizedGeneralSelector { public GeneralSelector(String base, Pair<Integer, Integer> zoom, List<Condition> conds, Subpart subpart) { super(base, zoom, conds, subpart); } public boolean matchesConditions(Environment e) { return super.matches(e); } @Override public Selector optimizedBaseCheck() { return new OptimizedGeneralSelector(this); } @Override public boolean matches(Environment e) { return matchesBase(e) && super.matches(e); } } /** * Superclass of {@link GeneralSelector}. Used to create an "optimized" copy of this selector that omits the base check. * @see Selector#optimizedBaseCheck */ class OptimizedGeneralSelector extends AbstractSelector { public final String base; public final Range range; public final Subpart subpart; public OptimizedGeneralSelector(String base, Pair<Integer, Integer> zoom, List<Condition> conds, Subpart subpart) { super(conds); this.base = base; if (zoom != null) { int a = zoom.a == null ? 0 : zoom.a; int b = zoom.b == null ? Integer.MAX_VALUE : zoom.b; if (a <= b) { range = fromLevel(a, b); } else { range = Range.ZERO_TO_INFINITY; } } else { range = Range.ZERO_TO_INFINITY; } this.subpart = subpart != null ? subpart : Subpart.DEFAULT_SUBPART; } public OptimizedGeneralSelector(String base, Range range, List<Condition> conds, Subpart subpart) { super(conds); this.base = base; this.range = range; this.subpart = subpart != null ? subpart : Subpart.DEFAULT_SUBPART; } public OptimizedGeneralSelector(GeneralSelector s) { this(s.base, s.range, s.conds, s.subpart); } @Override public Subpart getSubpart() { return subpart; } @Override public Range getRange() { return range; } public String getBase() { return base; } public boolean matchesBase(OsmPrimitiveType type) { if ("*".equals(base)) { return true; } else if (OsmPrimitiveType.NODE.equals(type)) { return "node".equals(base); } else if (OsmPrimitiveType.WAY.equals(type)) { return "way".equals(base) || "area".equals(base); } else if (OsmPrimitiveType.RELATION.equals(type)) { return "area".equals(base) || "relation".equals(base) || "canvas".equals(base); } return false; } public boolean matchesBase(OsmPrimitive p) { if (!matchesBase(p.getType())) { return false; } else { if (p instanceof Relation) { if ("area".equals(base)) { return ((Relation) p).isMultipolygon(); } else if ("canvas".equals(base)) { return p.get("#canvas") != null; } } return true; } } public boolean matchesBase(Environment e) { return matchesBase(e.osm); } @Override public Selector optimizedBaseCheck() { throw new UnsupportedOperationException(); } public static Range fromLevel(int a, int b) { if (a > b) throw new AssertionError(); double lower = 0; double upper = Double.POSITIVE_INFINITY; if (b != Integer.MAX_VALUE) { lower = level2scale(b + 1); } if (a != 0) { upper = level2scale(a); } return new Range(lower, upper); } public static double level2scale(int lvl) { if (lvl < 0) throw new IllegalArgumentException("lvl must be >= 0 but is "+lvl); // preliminary formula - map such that mapnik imagery tiles of the same // or similar level are displayed at the given scale return 2.0 * Math.PI * WGS84.a / Math.pow(2.0, lvl) / 2.56; } public static int scale2level(double scale) { if (scale < 0) throw new IllegalArgumentException("scale must be >= 0 but is "+scale); return (int) Math.floor(Math.log(2 * Math.PI * WGS84.a / 2.56 / scale) / Math.log(2)); } @Override public String toString() { return base + (Range.ZERO_TO_INFINITY.equals(range) ? "" : range) + Utils.join("", conds) + (subpart != null && subpart != Subpart.DEFAULT_SUBPART ? ("::" + subpart) : ""); } } }