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

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

sonar - squid:S1066 - Collapsible "if" statements should be merged

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