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

Last change on this file since 13811 was 13810, checked in by Don-vip, 6 years ago

support rendering of IPrimitive

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