source: josm/trunk/src/org/openstreetmap/josm/gui/mappaint/mapcss/Selector.java@ 8833

Last change on this file since 8833 was 8833, checked in by simon04, 9 years ago

fix #11939 - MapCSS: fix parent_tag() in ∈ and ⧉ tests

  • Property svn:eol-style set to native
File size: 24.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.mappaint.mapcss;
3
4import java.util.Collection;
5import java.util.Collections;
6import java.util.List;
7import java.util.NoSuchElementException;
8import java.util.regex.PatternSyntaxException;
9
10import org.openstreetmap.josm.Main;
11import org.openstreetmap.josm.data.osm.Node;
12import org.openstreetmap.josm.data.osm.OsmPrimitive;
13import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
14import org.openstreetmap.josm.data.osm.Relation;
15import org.openstreetmap.josm.data.osm.RelationMember;
16import org.openstreetmap.josm.data.osm.Way;
17import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
18import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
19import org.openstreetmap.josm.gui.mappaint.Environment;
20import org.openstreetmap.josm.gui.mappaint.Range;
21import org.openstreetmap.josm.tools.CheckParameterUtil;
22import org.openstreetmap.josm.tools.Geometry;
23import org.openstreetmap.josm.tools.Pair;
24import org.openstreetmap.josm.tools.Predicates;
25import org.openstreetmap.josm.tools.Utils;
26
27/**
28 * MapCSS selector.
29 *
30 * A rule has two parts, a selector and a declaration block
31 * e.g.
32 * <pre>
33 * way[highway=residential]
34 * { width: 10; color: blue; }
35 * </pre>
36 *
37 * The selector decides, if the declaration block gets applied or not.
38 *
39 * All implementing classes of Selector are immutable.
40 */
41public interface Selector {
42
43 /**
44 * Apply the selector to the primitive and check if it matches.
45 *
46 * @param env the Environment. env.mc and env.layer are read-only when matching a selector.
47 * env.source is not needed. This method will set the matchingReferrers field of env as
48 * a side effect! Make sure to clear it before invoking this method.
49 * @return true, if the selector applies
50 */
51 boolean matches(Environment env);
52
53 Subpart getSubpart();
54
55 Range getRange();
56
57 /**
58 * Create an "optimized" copy of this selector that omits the base check.
59 *
60 * For the style source, the list of rules is preprocessed, such that
61 * there is a separate list of rules for nodes, ways, ...
62 *
63 * This means that the base check does not have to be performed
64 * for each rule, but only once for each primitive.
65 *
66 * @return a selector that is identical to this object, except the base of the
67 * "rightmost" selector is not checked
68 */
69 Selector optimizedBaseCheck();
70
71 enum ChildOrParentSelectorType {
72 CHILD, PARENT, ELEMENT_OF, CROSSING, SIBLING
73 }
74
75 /**
76 * <p>Represents a child selector or a parent selector.</p>
77 *
78 * <p>In addition to the standard CSS notation for child selectors, JOSM also supports
79 * an "inverse" notation:</p>
80 * <pre>
81 * selector_a &gt; selector_b { ... } // the standard notation (child selector)
82 * relation[type=route] &gt; way { ... } // example (all ways of a route)
83 *
84 * selector_a &lt; selector_b { ... } // the inverse notation (parent selector)
85 * node[traffic_calming] &lt; way { ... } // example (way that has a traffic calming node)
86 * </pre>
87 *
88 */
89 class ChildOrParentSelector implements Selector {
90 public final Selector left;
91 public final LinkSelector link;
92 public final Selector right;
93 public final ChildOrParentSelectorType type;
94
95 /**
96 *
97 * @param a the first selector
98 * @param b the second selector
99 * @param type the selector type
100 */
101 public ChildOrParentSelector(Selector a, LinkSelector link, Selector b, ChildOrParentSelectorType type) {
102 CheckParameterUtil.ensureParameterNotNull(a, "a");
103 CheckParameterUtil.ensureParameterNotNull(b, "b");
104 CheckParameterUtil.ensureParameterNotNull(link, "link");
105 CheckParameterUtil.ensureParameterNotNull(type, "type");
106 this.left = a;
107 this.link = link;
108 this.right = b;
109 this.type = type;
110 }
111
112 /**
113 * <p>Finds the first referrer matching {@link #left}</p>
114 *
115 * <p>The visitor works on an environment and it saves the matching
116 * referrer in {@code e.parent} and its relative position in the
117 * list referrers "child list" in {@code e.index}.</p>
118 *
119 * <p>If after execution {@code e.parent} is null, no matching
120 * referrer was found.</p>
121 *
122 */
123 private class MatchingReferrerFinder extends AbstractVisitor{
124 private Environment e;
125
126 /**
127 * Constructor
128 * @param e the environment against which we match
129 */
130 public MatchingReferrerFinder(Environment e) {
131 this.e = e;
132 }
133
134 @Override
135 public void visit(Node n) {
136 // node should never be a referrer
137 throw new AssertionError();
138 }
139
140 @Override
141 public void visit(Way w) {
142 /*
143 * If e.parent is already set to the first matching referrer. We skip any following
144 * referrer injected into the visitor.
145 */
146 if (e.parent != null) return;
147
148 if (!left.matches(e.withPrimitive(w)))
149 return;
150 for (int i = 0; i < w.getNodesCount(); i++) {
151 Node n = w.getNode(i);
152 if (n.equals(e.osm)) {
153 if (link.matches(e.withParentAndIndexAndLinkContext(w, i, w.getNodesCount()))) {
154 e.parent = w;
155 e.index = i;
156 e.count = w.getNodesCount();
157 return;
158 }
159 }
160 }
161 }
162
163 @Override
164 public void visit(Relation r) {
165 /*
166 * If e.parent is already set to the first matching referrer. We skip any following
167 * referrer injected into the visitor.
168 */
169 if (e.parent != null) return;
170
171 if (!left.matches(e.withPrimitive(r)))
172 return;
173 for (int i = 0; i < r.getMembersCount(); i++) {
174 RelationMember m = r.getMember(i);
175 if (m.getMember().equals(e.osm)) {
176 if (link.matches(e.withParentAndIndexAndLinkContext(r, i, r.getMembersCount()))) {
177 e.parent = r;
178 e.index = i;
179 e.count = r.getMembersCount();
180 return;
181 }
182 }
183 }
184 }
185 }
186
187 private abstract class AbstractFinder extends AbstractVisitor {
188 protected final Environment e;
189
190 protected AbstractFinder(Environment e) {
191 this.e = e;
192 }
193
194 @Override
195 public void visit(Node n) {
196 }
197
198 @Override
199 public void visit(Way w) {
200 }
201
202 @Override
203 public void visit(Relation r) {
204 }
205
206 public void visit(Collection<? extends OsmPrimitive> primitives) {
207 for (OsmPrimitive p : primitives) {
208 if (e.child != null) {
209 // abort if first match has been found
210 break;
211 } else if (isPrimitiveUsable(p)) {
212 p.accept(this);
213 }
214 }
215 }
216
217 public boolean isPrimitiveUsable(OsmPrimitive p) {
218 return !e.osm.equals(p) && p.isUsable();
219 }
220 }
221
222 private class MultipolygonOpenEndFinder extends AbstractFinder {
223
224 @Override
225 public void visit(Way w) {
226 w.visitReferrers(innerVisitor);
227 }
228
229 public MultipolygonOpenEndFinder(Environment e) {
230 super(e);
231 }
232
233 private final AbstractVisitor innerVisitor = new AbstractFinder(e) {
234 @Override
235 public void visit(Relation r) {
236 if (left.matches(e.withPrimitive(r))) {
237 final List<Node> openEnds = MultipolygonCache.getInstance().get(Main.map.mapView, r).getOpenEnds();
238 final int openEndIndex = openEnds.indexOf(e.osm);
239 if (openEndIndex >= 0) {
240 e.parent = r;
241 e.index = openEndIndex;
242 e.count = openEnds.size();
243 }
244 }
245 }
246 };
247
248 }
249
250 private final class CrossingFinder extends AbstractFinder {
251 private CrossingFinder(Environment e) {
252 super(e);
253 CheckParameterUtil.ensureThat(e.osm instanceof Way, "Only ways are supported");
254 }
255
256 @Override
257 public void visit(Way w) {
258 if (e.child == null && left.matches(new Environment(w).withParent(e.osm))) {
259 if (e.osm instanceof Way && Geometry.PolygonIntersection.CROSSING.equals(
260 Geometry.polygonIntersection(w.getNodes(), ((Way) e.osm).getNodes()))) {
261 e.child = w;
262 }
263 }
264 }
265 }
266
267 private class ContainsFinder extends AbstractFinder {
268 protected ContainsFinder(Environment e) {
269 super(e);
270 CheckParameterUtil.ensureThat(!(e.osm instanceof Node), "Nodes not supported");
271 }
272
273 @Override
274 public void visit(Node n) {
275 if (e.child == null && left.matches(new Environment(n).withParent(e.osm))) {
276 if (e.osm instanceof Way && Geometry.nodeInsidePolygon(n, ((Way) e.osm).getNodes())
277 || e.osm instanceof Relation && (
278 (Relation) e.osm).isMultipolygon() && Geometry.isNodeInsideMultiPolygon(n, (Relation) e.osm, null)) {
279 e.child = n;
280 }
281 }
282 }
283
284 @Override
285 public void visit(Way w) {
286 if (e.child == null && left.matches(new Environment(w).withParent(e.osm))) {
287 if (e.osm instanceof Way && Geometry.PolygonIntersection.FIRST_INSIDE_SECOND.equals(
288 Geometry.polygonIntersection(w.getNodes(), ((Way) e.osm).getNodes()))
289 || e.osm instanceof Relation && (
290 (Relation) e.osm).isMultipolygon()
291 && Geometry.isPolygonInsideMultiPolygon(w.getNodes(), (Relation) e.osm, null)) {
292 e.child = w;
293 }
294 }
295 }
296 }
297
298 @Override
299 public boolean matches(Environment e) {
300
301 if (!right.matches(e))
302 return false;
303
304 if (ChildOrParentSelectorType.ELEMENT_OF.equals(type)) {
305
306 if (e.osm instanceof Node || e.osm.getDataSet() == null) {
307 // nodes cannot contain elements
308 return false;
309 }
310
311 ContainsFinder containsFinder;
312 try {
313 // if right selector also matches relations and if matched primitive is a way which is part of a multipolygon,
314 // use the multipolygon for further analysis
315 if (!(e.osm instanceof Way)
316 || (right instanceof OptimizedGeneralSelector
317 && !((OptimizedGeneralSelector) right).matchesBase(OsmPrimitiveType.RELATION))) {
318 throw new NoSuchElementException();
319 }
320 final Collection<Relation> multipolygons = Utils.filteredCollection(Utils.filter(
321 e.osm.getReferrers(), Predicates.hasTag("type", "multipolygon")), Relation.class);
322 final Relation multipolygon = multipolygons.iterator().next();
323 if (multipolygon == null) throw new NoSuchElementException();
324 containsFinder = new ContainsFinder(new Environment(multipolygon)) {
325 @Override
326 public boolean isPrimitiveUsable(OsmPrimitive p) {
327 return super.isPrimitiveUsable(p) && !multipolygon.getMemberPrimitives().contains(p);
328 }
329 };
330 } catch (NoSuchElementException ignore) {
331 containsFinder = new ContainsFinder(e);
332 }
333 e.parent = e.osm;
334
335 if (left instanceof OptimizedGeneralSelector) {
336 if (((OptimizedGeneralSelector) left).matchesBase(OsmPrimitiveType.NODE)) {
337 containsFinder.visit(e.osm.getDataSet().searchNodes(e.osm.getBBox()));
338 }
339 if (((OptimizedGeneralSelector) left).matchesBase(OsmPrimitiveType.WAY)) {
340 containsFinder.visit(e.osm.getDataSet().searchWays(e.osm.getBBox()));
341 }
342 } else {
343 // use slow test
344 containsFinder.visit(e.osm.getDataSet().allPrimitives());
345 }
346
347 return e.child != null;
348
349 } else if (ChildOrParentSelectorType.CROSSING.equals(type) && e.osm instanceof Way) {
350 e.parent = e.osm;
351 final CrossingFinder crossingFinder = new CrossingFinder(e);
352 if (right instanceof OptimizedGeneralSelector
353 && ((OptimizedGeneralSelector) right).matchesBase(OsmPrimitiveType.WAY)) {
354 crossingFinder.visit(e.osm.getDataSet().searchWays(e.osm.getBBox()));
355 }
356 return e.child != null;
357 } else if (ChildOrParentSelectorType.SIBLING.equals(type)) {
358 if (e.osm instanceof Node) {
359 for (Way w : Utils.filteredCollection(e.osm.getReferrers(true), Way.class)) {
360 final int i = w.getNodes().indexOf(e.osm);
361 if (i - 1 >= 0) {
362 final Node n = w.getNode(i - 1);
363 final Environment e2 = e.withPrimitive(n).withParent(w).withChild(e.osm);
364 if (left.matches(e2) && link.matches(e2.withLinkContext())) {
365 e.child = n;
366 e.index = i;
367 e.count = w.getNodesCount();
368 e.parent = w;
369 return true;
370 }
371 }
372 }
373 }
374 } else if (ChildOrParentSelectorType.CHILD.equals(type)
375 && link.conds != null && !link.conds.isEmpty()
376 && link.conds.get(0) instanceof Condition.OpenEndPseudoClassCondition) {
377 if (e.osm instanceof Node) {
378 e.osm.visitReferrers(new MultipolygonOpenEndFinder(e));
379 return e.parent != null;
380 }
381 } else if (ChildOrParentSelectorType.CHILD.equals(type)) {
382 MatchingReferrerFinder collector = new MatchingReferrerFinder(e);
383 e.osm.visitReferrers(collector);
384 if (e.parent != null)
385 return true;
386 } else if (ChildOrParentSelectorType.PARENT.equals(type)) {
387 if (e.osm instanceof Way) {
388 List<Node> wayNodes = ((Way) e.osm).getNodes();
389 for (int i = 0; i < wayNodes.size(); i++) {
390 Node n = wayNodes.get(i);
391 if (left.matches(e.withPrimitive(n))) {
392 if (link.matches(e.withChildAndIndexAndLinkContext(n, i, wayNodes.size()))) {
393 e.child = n;
394 e.index = i;
395 e.count = wayNodes.size();
396 return true;
397 }
398 }
399 }
400 } else if (e.osm instanceof Relation) {
401 List<RelationMember> members = ((Relation) e.osm).getMembers();
402 for (int i = 0; i < members.size(); i++) {
403 OsmPrimitive member = members.get(i).getMember();
404 if (left.matches(e.withPrimitive(member))) {
405 if (link.matches(e.withChildAndIndexAndLinkContext(member, i, members.size()))) {
406 e.child = member;
407 e.index = i;
408 e.count = members.size();
409 return true;
410 }
411 }
412 }
413 }
414 }
415 return false;
416 }
417
418 @Override
419 public Subpart getSubpart() {
420 return right.getSubpart();
421 }
422
423 @Override
424 public Range getRange() {
425 return right.getRange();
426 }
427
428 @Override
429 public Selector optimizedBaseCheck() {
430 return new ChildOrParentSelector(left, link, right.optimizedBaseCheck(), type);
431 }
432
433 @Override
434 public String toString() {
435 return left + " " + (ChildOrParentSelectorType.PARENT.equals(type) ? "<" : ">") + link + " " + right;
436 }
437 }
438
439 /**
440 * Super class of {@link org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector} and
441 * {@link org.openstreetmap.josm.gui.mappaint.mapcss.Selector.LinkSelector}.
442 * @since 5841
443 */
444 abstract class AbstractSelector implements Selector {
445
446 protected final List<Condition> conds;
447
448 protected AbstractSelector(List<Condition> conditions) {
449 if (conditions == null || conditions.isEmpty()) {
450 this.conds = null;
451 } else {
452 this.conds = conditions;
453 }
454 }
455
456 /**
457 * Determines if all conditions match the given environment.
458 * @param env The environment to check
459 * @return {@code true} if all conditions apply, false otherwise.
460 */
461 @Override
462 public boolean matches(Environment env) {
463 if (conds == null) return true;
464 for (Condition c : conds) {
465 try {
466 if (!c.applies(env)) return false;
467 } catch (PatternSyntaxException e) {
468 Main.error("PatternSyntaxException while applying condition" + c +": "+e.getMessage());
469 return false;
470 }
471 }
472 return true;
473 }
474
475 /**
476 * Returns the list of conditions.
477 * @return the list of conditions
478 */
479 public List<Condition> getConditions() {
480 if (conds == null) {
481 return Collections.emptyList();
482 }
483 return Collections.unmodifiableList(conds);
484 }
485 }
486
487 class LinkSelector extends AbstractSelector {
488
489 public LinkSelector(List<Condition> conditions) {
490 super(conditions);
491 }
492
493 @Override
494 public boolean matches(Environment env) {
495 Utils.ensure(env.isLinkContext(), "Requires LINK context in environment, got ''{0}''", env.getContext());
496 return super.matches(env);
497 }
498
499 @Override
500 public Subpart getSubpart() {
501 throw new UnsupportedOperationException("Not supported yet.");
502 }
503
504 @Override
505 public Range getRange() {
506 throw new UnsupportedOperationException("Not supported yet.");
507 }
508
509 @Override
510 public Selector optimizedBaseCheck() {
511 throw new UnsupportedOperationException();
512 }
513
514 @Override
515 public String toString() {
516 return "LinkSelector{" + "conditions=" + conds + '}';
517 }
518 }
519
520 class GeneralSelector extends OptimizedGeneralSelector {
521
522 public GeneralSelector(String base, Pair<Integer, Integer> zoom, List<Condition> conds, Subpart subpart) {
523 super(base, zoom, conds, subpart);
524 }
525
526 public boolean matchesConditions(Environment e) {
527 return super.matches(e);
528 }
529
530 @Override
531 public Selector optimizedBaseCheck() {
532 return new OptimizedGeneralSelector(this);
533 }
534
535 @Override
536 public boolean matches(Environment e) {
537 return matchesBase(e) && super.matches(e);
538 }
539 }
540
541 class OptimizedGeneralSelector extends AbstractSelector {
542 public final String base;
543 public final Range range;
544 public final Subpart subpart;
545
546 public OptimizedGeneralSelector(String base, Pair<Integer, Integer> zoom, List<Condition> conds, Subpart subpart) {
547 super(conds);
548 this.base = base;
549 if (zoom != null) {
550 int a = zoom.a == null ? 0 : zoom.a;
551 int b = zoom.b == null ? Integer.MAX_VALUE : zoom.b;
552 if (a <= b) {
553 range = fromLevel(a, b);
554 } else {
555 range = Range.ZERO_TO_INFINITY;
556 }
557 } else {
558 range = Range.ZERO_TO_INFINITY;
559 }
560 this.subpart = subpart != null ? subpart : Subpart.DEFAULT_SUBPART;
561 }
562
563 public OptimizedGeneralSelector(String base, Range range, List<Condition> conds, Subpart subpart) {
564 super(conds);
565 this.base = base;
566 this.range = range;
567 this.subpart = subpart != null ? subpart : Subpart.DEFAULT_SUBPART;
568 }
569
570 public OptimizedGeneralSelector(GeneralSelector s) {
571 this(s.base, s.range, s.conds, s.subpart);
572 }
573
574 @Override
575 public Subpart getSubpart() {
576 return subpart;
577 }
578
579 @Override
580 public Range getRange() {
581 return range;
582 }
583
584 public String getBase() {
585 return base;
586 }
587
588 public boolean matchesBase(OsmPrimitiveType type) {
589 if ("*".equals(base)) {
590 return true;
591 } else if (OsmPrimitiveType.NODE.equals(type)) {
592 return "node".equals(base);
593 } else if (OsmPrimitiveType.WAY.equals(type)) {
594 return "way".equals(base) || "area".equals(base);
595 } else if (OsmPrimitiveType.RELATION.equals(type)) {
596 return "area".equals(base) || "relation".equals(base) || "canvas".equals(base);
597 }
598 return false;
599 }
600
601 public boolean matchesBase(OsmPrimitive p) {
602 if (!matchesBase(p.getType())) {
603 return false;
604 } else {
605 if (p instanceof Relation) {
606 if ("area".equals(base)) {
607 return ((Relation) p).isMultipolygon();
608 } else if ("canvas".equals(base)) {
609 return p.get("#canvas") != null;
610 }
611 }
612 return true;
613 }
614 }
615
616 public boolean matchesBase(Environment e) {
617 return matchesBase(e.osm);
618 }
619
620 @Override
621 public Selector optimizedBaseCheck() {
622 throw new UnsupportedOperationException();
623 }
624
625 public static Range fromLevel(int a, int b) {
626 if (a > b)
627 throw new AssertionError();
628 double lower = 0;
629 double upper = Double.POSITIVE_INFINITY;
630 if (b != Integer.MAX_VALUE) {
631 lower = level2scale(b + 1);
632 }
633 if (a != 0) {
634 upper = level2scale(a);
635 }
636 return new Range(lower, upper);
637 }
638
639 private static final double R = 6378135;
640
641 public static double level2scale(int lvl) {
642 if (lvl < 0)
643 throw new IllegalArgumentException("lvl must be >= 0 but is "+lvl);
644 // preliminary formula - map such that mapnik imagery tiles of the same
645 // or similar level are displayed at the given scale
646 return 2.0 * Math.PI * R / Math.pow(2.0, lvl) / 2.56;
647 }
648
649 public static int scale2level(double scale) {
650 if (scale < 0)
651 throw new IllegalArgumentException("scale must be >= 0 but is "+scale);
652 return (int) Math.floor(Math.log(2 * Math.PI * R / 2.56 / scale) / Math.log(2));
653 }
654
655 @Override
656 public String toString() {
657 return base + (Range.ZERO_TO_INFINITY.equals(range) ? "" : range) + Utils.join("", conds)
658 + (subpart != null && subpart != Subpart.DEFAULT_SUBPART ? "::" + subpart : "");
659 }
660 }
661}
Note: See TracBrowser for help on using the repository browser.