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

Last change on this file since 10837 was 10837, checked in by Don-vip, 8 years ago

see #11924 - extract MapCSS conditions to new class ConditionFactory (on the same model than ExpressionFactory) - should workaround Groovy bug with Java 9 (https://issues.apache.org/jira/browse/GROOVY-7879 ?)

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