001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.mappaint.mapcss; 003 004import static org.openstreetmap.josm.data.projection.Ellipsoid.WGS84; 005 006import java.awt.geom.Area; 007import java.awt.geom.Point2D; 008import java.text.MessageFormat; 009import java.util.ArrayList; 010import java.util.Arrays; 011import java.util.Collection; 012import java.util.Collections; 013import java.util.HashMap; 014import java.util.LinkedHashSet; 015import java.util.List; 016import java.util.Map; 017import java.util.Objects; 018import java.util.Set; 019import java.util.stream.Collectors; 020 021import org.openstreetmap.josm.data.osm.INode; 022import org.openstreetmap.josm.data.osm.IPrimitive; 023import org.openstreetmap.josm.data.osm.IRelation; 024import org.openstreetmap.josm.data.osm.IRelationMember; 025import org.openstreetmap.josm.data.osm.IWay; 026import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 027import org.openstreetmap.josm.data.osm.OsmUtils; 028import org.openstreetmap.josm.data.osm.Relation; 029import org.openstreetmap.josm.data.osm.Way; 030import org.openstreetmap.josm.data.osm.WaySegment; 031import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor; 032import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache; 033import org.openstreetmap.josm.data.validation.tests.CrossingWays; 034import org.openstreetmap.josm.gui.mappaint.Environment; 035import org.openstreetmap.josm.gui.mappaint.Range; 036import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.IndexCondition; 037import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.OpenEndPseudoClassCondition; 038import org.openstreetmap.josm.tools.CheckParameterUtil; 039import org.openstreetmap.josm.tools.CompositeList; 040import org.openstreetmap.josm.tools.Geometry; 041import org.openstreetmap.josm.tools.Geometry.PolygonIntersection; 042import org.openstreetmap.josm.tools.Logging; 043import org.openstreetmap.josm.tools.Pair; 044import org.openstreetmap.josm.tools.Utils; 045 046/** 047 * MapCSS selector. 048 * 049 * A rule has two parts, a selector and a declaration block 050 * e.g. 051 * <pre> 052 * way[highway=residential] 053 * { width: 10; color: blue; } 054 * </pre> 055 * 056 * The selector decides, if the declaration block gets applied or not. 057 * 058 * All implementing classes of Selector are immutable. 059 */ 060public interface Selector { 061 062 /** selector base that matches anything. */ 063 String BASE_ANY = "*"; 064 065 /** selector base that matches on OSM object node. */ 066 String BASE_NODE = "node"; 067 068 /** selector base that matches on OSM object way. */ 069 String BASE_WAY = "way"; 070 071 /** selector base that matches on OSM object relation. */ 072 String BASE_RELATION = "relation"; 073 074 /** selector base that matches with any area regardless of whether the area border is only modelled with a single way or with 075 * a set of ways glued together with a relation.*/ 076 String BASE_AREA = "area"; 077 078 /** selector base for special rules containing meta information. */ 079 String BASE_META = "meta"; 080 081 /** selector base for style information not specific to nodes, ways or relations. */ 082 String BASE_CANVAS = "canvas"; 083 084 /** selector base for artificial bases created to use preferences. */ 085 String BASE_SETTING = "setting"; 086 087 /** selector base for grouping settings. */ 088 String BASE_SETTINGS = "settings"; 089 090 /** 091 * Apply the selector to the primitive and check if it matches. 092 * 093 * @param env the Environment. env.mc and env.layer are read-only when matching a selector. 094 * env.source is not needed. This method will set the matchingReferrers field of env as 095 * a side effect! Make sure to clear it before invoking this method. 096 * @return true, if the selector applies 097 */ 098 boolean matches(Environment env); 099 100 /** 101 * Returns the subpart, if supported. A subpart identifies different rendering layers (<code>::subpart</code> syntax). 102 * @return the subpart, if supported 103 * @throws UnsupportedOperationException if not supported 104 */ 105 Subpart getSubpart(); 106 107 /** 108 * Returns the scale range, an interval of the form "lower < x <= upper" where 0 <= lower < upper. 109 * @return the scale range, if supported 110 * @throws UnsupportedOperationException if not supported 111 */ 112 Range getRange(); 113 114 String getBase(); 115 116 /** 117 * Returns the list of conditions. 118 * @return the list of conditions 119 */ 120 List<Condition> getConditions(); 121 122 /** 123 * The type of child of parent selector. 124 * @see ChildOrParentSelector 125 */ 126 enum ChildOrParentSelectorType { 127 CHILD, PARENT, SUBSET_OR_EQUAL, NOT_SUBSET_OR_EQUAL, SUPERSET_OR_EQUAL, NOT_SUPERSET_OR_EQUAL, CROSSING, SIBLING, 128 } 129 130 /** 131 * <p>Represents a child selector or a parent selector.</p> 132 * 133 * <p>In addition to the standard CSS notation for child selectors, JOSM also supports 134 * an "inverse" notation:</p> 135 * <pre> 136 * selector_a > selector_b { ... } // the standard notation (child selector) 137 * relation[type=route] > way { ... } // example (all ways of a route) 138 * 139 * selector_a < selector_b { ... } // the inverse notation (parent selector) 140 * node[traffic_calming] < way { ... } // example (way that has a traffic calming node) 141 * </pre> 142 * <p>Child: see <a href="https://josm.openstreetmap.de/wiki/Help/Styles/MapCSSImplementation#Childselector">wiki</a> 143 * <br>Parent: see <a href="https://josm.openstreetmap.de/wiki/Help/Styles/MapCSSImplementation#Parentselector">wiki</a></p> 144 */ 145 class ChildOrParentSelector implements Selector { 146 public final Selector left; 147 public final LinkSelector link; 148 public final Selector right; 149 public final ChildOrParentSelectorType type; 150 151 /** 152 * Constructs a new {@code ChildOrParentSelector}. 153 * @param a the first selector 154 * @param link link 155 * @param b the second selector 156 * @param type the selector type 157 */ 158 public ChildOrParentSelector(Selector a, LinkSelector link, Selector b, ChildOrParentSelectorType type) { 159 CheckParameterUtil.ensureParameterNotNull(a, "a"); 160 CheckParameterUtil.ensureParameterNotNull(b, "b"); 161 CheckParameterUtil.ensureParameterNotNull(link, "link"); 162 CheckParameterUtil.ensureParameterNotNull(type, "type"); 163 this.left = a; 164 this.link = link; 165 this.right = b; 166 this.type = type; 167 } 168 169 @Override 170 public String getBase() { 171 // take the base from the rightmost selector 172 return right.getBase(); 173 } 174 175 @Override 176 public List<Condition> getConditions() { 177 return new CompositeList<>(left.getConditions(), right.getConditions()); 178 } 179 180 /** 181 * <p>Finds the first referrer matching {@link #left}</p> 182 * 183 * <p>The visitor works on an environment and it saves the matching 184 * referrer in {@code e.parent} and its relative position in the 185 * list referrers "child list" in {@code e.index}.</p> 186 * 187 * <p>If after execution {@code e.parent} is null, no matching 188 * referrer was found.</p> 189 * 190 */ 191 private class MatchingReferrerFinder implements PrimitiveVisitor { 192 private final Environment e; 193 194 /** 195 * Constructor 196 * @param e the environment against which we match 197 */ 198 MatchingReferrerFinder(Environment e) { 199 this.e = e; 200 } 201 202 @Override 203 public void visit(INode n) { 204 // node should never be a referrer 205 throw new AssertionError(); 206 } 207 208 private void doVisit(IPrimitive parent) { 209 // If e.parent is already set to the first matching referrer. 210 // We skip any following referrer injected into the visitor. 211 if (e.parent != null) return; 212 213 IPrimitive osm = e.osm; 214 try { 215 e.osm = parent; 216 if (!left.matches(e)) 217 return; 218 } finally { 219 e.osm = osm; 220 } 221 int count = parent instanceof IWay<?> 222 ? ((IWay<?>) parent).getNodesCount() 223 : ((IRelation<?>) parent).getMembersCount(); 224 if (link.getConditions().isEmpty()) { 225 // index is not needed, we can avoid the sequential search below 226 e.parent = parent; 227 e.count = count; 228 return; 229 } 230 // see #18964 231 int step = firstAndLastOnly() ? count - 1 : 1; 232 for (int i = 0; i < count; i += step) { 233 IPrimitive o = parent instanceof IWay<?> 234 ? ((IWay<?>) parent).getNode(i) 235 : ((IRelation<?>) parent).getMember(i).getMember(); 236 if (Objects.equals(o, e.osm) 237 && link.matches(e.withParentAndIndexAndLinkContext(parent, i, count))) { 238 e.parent = parent; 239 e.index = i; 240 e.count = count; 241 return; 242 } 243 } 244 } 245 246 private boolean firstAndLastOnly() { 247 return link.getConditions().stream().allMatch(c -> c instanceof IndexCondition && ((IndexCondition) c).isFirstOrLast); 248 } 249 250 @Override 251 public void visit(IWay<?> w) { 252 doVisit(w); 253 } 254 255 @Override 256 public void visit(IRelation<?> r) { 257 doVisit(r); 258 } 259 } 260 261 private abstract static class AbstractFinder implements PrimitiveVisitor { 262 protected final Environment e; 263 264 protected AbstractFinder(Environment e) { 265 this.e = e; 266 } 267 268 @Override 269 public void visit(INode n) { 270 } 271 272 @Override 273 public void visit(IWay<?> w) { 274 } 275 276 @Override 277 public void visit(IRelation<?> r) { 278 } 279 280 public void visit(Collection<? extends IPrimitive> primitives) { 281 for (IPrimitive p : primitives) { 282 if (e.child != null) { 283 // abort if first match has been found 284 break; 285 } else if (isPrimitiveUsable(p)) { 286 p.accept(this); 287 } 288 } 289 } 290 291 public boolean isPrimitiveUsable(IPrimitive p) { 292 return !e.osm.equals(p) && p.isUsable(); 293 } 294 295 protected void addToChildren(Environment e, IPrimitive p) { 296 if (e.children == null) { 297 e.children = new LinkedHashSet<>(); 298 } 299 e.children.add(p); 300 } 301 } 302 303 private class MultipolygonOpenEndFinder extends AbstractFinder { 304 305 @Override 306 public void visit(IWay<?> w) { 307 w.visitReferrers(innerVisitor); 308 } 309 310 MultipolygonOpenEndFinder(Environment e) { 311 super(e); 312 } 313 314 private final PrimitiveVisitor innerVisitor = new AbstractFinder(e) { 315 @Override 316 public void visit(IRelation<?> r) { 317 if (r instanceof Relation && left.matches(e.withPrimitive(r))) { 318 final List<?> openEnds = MultipolygonCache.getInstance().get((Relation) r).getOpenEnds(); 319 final int openEndIndex = openEnds.indexOf(e.osm); 320 if (openEndIndex >= 0) { 321 e.parent = r; 322 e.index = openEndIndex; 323 e.count = openEnds.size(); 324 } 325 } 326 } 327 }; 328 } 329 330 private final class CrossingFinder extends AbstractFinder { 331 332 private final String layer; 333 private Area area; 334 /** Will contain all way segments, grouped by cells */ 335 Map<Point2D, List<WaySegment>> cellSegments; 336 337 private CrossingFinder(Environment e) { 338 super(e); 339 CheckParameterUtil.ensureThat(isArea(e.osm), "Only areas are supported"); 340 layer = OsmUtils.getLayer(e.osm); 341 } 342 343 private Area getAreaEastNorth(IPrimitive p, Environment e) { 344 if (e.mpAreaCache != null && p.isMultipolygon()) { 345 Area a = e.mpAreaCache.get(p); 346 if (a == null) { 347 a = Geometry.getAreaEastNorth(p); 348 e.mpAreaCache.put(p, a); 349 } 350 return a; 351 } 352 return Geometry.getAreaEastNorth(p); 353 } 354 355 private Map<List<Way>, List<WaySegment>> findCrossings(IPrimitive area, 356 Map<Point2D, List<WaySegment>> cellSegments) { 357 /** The detected crossing ways */ 358 Map<List<Way>, List<WaySegment>> crossingWays = new HashMap<>(50); 359 if (area instanceof Way) { 360 CrossingWays.findIntersectingWay((Way) area, cellSegments, crossingWays, false); 361 } else if (area instanceof Relation && area.isMultipolygon()) { 362 Relation r = (Relation) area; 363 for (Way w : r.getMemberPrimitives(Way.class)) { 364 if (!w.hasIncompleteNodes()) { 365 CrossingWays.findIntersectingWay(w, cellSegments, crossingWays, false); 366 } 367 } 368 } 369 return crossingWays; 370 } 371 372 @Override 373 public void visit(Collection<? extends IPrimitive> primitives) { 374 Set<? extends IPrimitive> toIgnore; 375 if (e.osm instanceof Relation) { 376 toIgnore = ((Relation) e.osm).getMemberPrimitives(); 377 } else { 378 toIgnore = null; 379 } 380 boolean filterWithTested = e.toMatchForSurrounding != null && !e.toMatchForSurrounding.isEmpty(); 381 for (IPrimitive p : primitives) { 382 if (filterWithTested && !e.toMatchForSurrounding.contains(p)) 383 continue; 384 if (isPrimitiveUsable(p) && Objects.equals(layer, OsmUtils.getLayer(p)) 385 && left.matches(new Environment(p).withParent(e.osm)) && isArea(p) 386 && (toIgnore == null || !toIgnore.contains(p))) { 387 if (e.osm instanceof Way && ((Way) e.osm).referrers(Relation.class).anyMatch(ref -> ref == p)) 388 continue; 389 visitArea(p); 390 } 391 } 392 } 393 394 private void visitArea(IPrimitive p) { 395 if (area == null) { 396 area = getAreaEastNorth(e.osm, e); 397 } 398 Area otherArea = getAreaEastNorth(p, e); 399 if (area.isEmpty() || otherArea.isEmpty()) { 400 useFindCrossings(p); 401 } else { 402 // we have complete data. This allows to find intersections with shared nodes 403 // See #16707 404 Pair<PolygonIntersection, Area> is = Geometry.polygonIntersectionResult( 405 otherArea, area, Geometry.INTERSECTION_EPS_EAST_NORTH); 406 if (Geometry.PolygonIntersection.CROSSING == is.a) { 407 addToChildren(e, p); 408 // store intersection area to improve highlight and zoom to problem 409 if (e.intersections == null) { 410 e.intersections = new HashMap<>(); 411 } 412 e.intersections.put(p, is.b); 413 } 414 } 415 416 } 417 418 private void useFindCrossings(IPrimitive p) { 419 if (cellSegments == null) { 420 // lazy initialisation 421 cellSegments = new HashMap<>(); 422 findCrossings(e.osm, cellSegments); // ignore self intersections etc. here 423 } 424 // need a copy 425 final Map<Point2D, List<WaySegment>> tmpCellSegments = new HashMap<>(cellSegments); 426 // calculate all crossings between e.osm and p 427 Map<List<Way>, List<WaySegment>> crossingWays = findCrossings(p, tmpCellSegments); 428 if (!crossingWays.isEmpty()) { 429 addToChildren(e, p); 430 if (e.crossingWaysMap == null) { 431 e.crossingWaysMap = new HashMap<>(); 432 } 433 e.crossingWaysMap.put(p, crossingWays); 434 } 435 } 436 } 437 438 /** 439 * Finds elements which are inside the right element, collects those in {@code children} 440 */ 441 private class ContainsFinder extends AbstractFinder { 442 protected List<IPrimitive> toCheck; 443 444 protected ContainsFinder(Environment e) { 445 super(e); 446 CheckParameterUtil.ensureThat(!(e.osm instanceof INode), "Nodes not supported"); 447 } 448 449 @Override 450 public void visit(Collection<? extends IPrimitive> primitives) { 451 for (IPrimitive p : primitives) { 452 if (p != e.osm && isPrimitiveUsable(p) && left.matches(new Environment(p).withParent(e.osm))) { 453 if (toCheck == null) { 454 toCheck = new ArrayList<>(); 455 } 456 toCheck.add(p); 457 } 458 } 459 } 460 461 void execGeometryTests() { 462 if (Utils.isEmpty(toCheck)) 463 return; 464 for (IPrimitive p : Geometry.filterInsideAnyPolygon(toCheck, e.osm)) { 465 addToChildren(e, p); 466 } 467 } 468 } 469 470 /** 471 * Finds elements which are inside the left element, or in other words, it finds elements enclosing e.osm. 472 * The found enclosing elements are collected in {@code e.children}. 473 */ 474 private class InsideOrEqualFinder extends AbstractFinder { 475 476 protected InsideOrEqualFinder(Environment e) { 477 super(e); 478 } 479 480 @Override 481 public void visit(IWay<?> w) { 482 if (left.matches(new Environment(w).withParent(e.osm)) 483 && w.getBBox().bounds(e.osm.getBBox()) 484 && !Geometry.filterInsidePolygon(Collections.singletonList(e.osm), w).isEmpty()) { 485 addToChildren(e, w); 486 } 487 } 488 489 @Override 490 public void visit(IRelation<?> r) { 491 if (r instanceof Relation && r.isMultipolygon() && r.getBBox().bounds(e.osm.getBBox()) 492 && left.matches(new Environment(r).withParent(e.osm)) 493 && !Geometry.filterInsideMultipolygon(Collections.singletonList(e.osm), (Relation) r).isEmpty()) { 494 addToChildren(e, r); 495 } 496 } 497 } 498 499 private void visitBBox(Environment e, AbstractFinder finder) { 500 boolean withNodes = finder instanceof ContainsFinder; 501 if (e.osm.getDataSet() == null) { 502 // do nothing 503 } else if (left instanceof GeneralSelector) { 504 if (withNodes && ((GeneralSelector) left).matchesBase(OsmPrimitiveType.NODE)) { 505 finder.visit(e.osm.getDataSet().searchNodes(e.osm.getBBox())); 506 } 507 if (((GeneralSelector) left).matchesBase(OsmPrimitiveType.WAY)) { 508 finder.visit(e.osm.getDataSet().searchWays(e.osm.getBBox())); 509 } 510 if (((GeneralSelector) left).matchesBase(OsmPrimitiveType.RELATION)) { 511 finder.visit(e.osm.getDataSet().searchRelations(e.osm.getBBox())); 512 } 513 } else { 514 if (withNodes) { 515 finder.visit(e.osm.getDataSet().searchNodes(e.osm.getBBox())); 516 } 517 finder.visit(e.osm.getDataSet().searchWays(e.osm.getBBox())); 518 finder.visit(e.osm.getDataSet().searchRelations(e.osm.getBBox())); 519 } 520 } 521 522 private static boolean isArea(IPrimitive p) { 523 return (p instanceof IWay && ((IWay<?>) p).isClosed() && ((IWay<?>) p).getNodesCount() >= 4) 524 || (p instanceof IRelation && p.isMultipolygon() && !p.isIncomplete()); 525 } 526 527 @Override 528 public boolean matches(Environment e) { 529 530 if (!right.matches(e)) 531 return false; 532 533 if (ChildOrParentSelectorType.SUBSET_OR_EQUAL == type || ChildOrParentSelectorType.NOT_SUBSET_OR_EQUAL == type) { 534 535 if (e.osm.getDataSet() == null || !isArea(e.osm)) { 536 // only areas can contain elements 537 return ChildOrParentSelectorType.NOT_SUBSET_OR_EQUAL == type; 538 } 539 ContainsFinder containsFinder = new ContainsFinder(e); 540 e.parent = e.osm; 541 542 visitBBox(e, containsFinder); 543 containsFinder.execGeometryTests(); 544 return ChildOrParentSelectorType.SUBSET_OR_EQUAL == type ? e.children != null : e.children == null; 545 546 } else if (ChildOrParentSelectorType.SUPERSET_OR_EQUAL == type || ChildOrParentSelectorType.NOT_SUPERSET_OR_EQUAL == type) { 547 548 if (e.osm.getDataSet() == null || (e.osm instanceof INode && ((INode) e.osm).getCoor() == null) 549 || (!(e.osm instanceof INode) && !isArea(e.osm))) { 550 return ChildOrParentSelectorType.NOT_SUPERSET_OR_EQUAL == type; 551 } 552 553 InsideOrEqualFinder insideOrEqualFinder = new InsideOrEqualFinder(e); 554 e.parent = e.osm; 555 556 visitBBox(e, insideOrEqualFinder); 557 return ChildOrParentSelectorType.SUPERSET_OR_EQUAL == type ? e.children != null : e.children == null; 558 559 } else if (ChildOrParentSelectorType.CROSSING == type) { 560 e.parent = e.osm; 561 if (e.osm.getDataSet() != null && isArea(e.osm)) { 562 final CrossingFinder crossingFinder = new CrossingFinder(e); 563 visitBBox(e, crossingFinder); 564 return e.children != null; 565 } 566 return e.children != null; 567 } else if (ChildOrParentSelectorType.SIBLING == type) { 568 if (e.osm instanceof INode) { 569 for (IPrimitive ref : e.osm.getReferrers(true)) { 570 if (ref instanceof IWay) { 571 IWay<?> w = (IWay<?>) ref; 572 final int i = w.getNodes().indexOf(e.osm); 573 if (i - 1 >= 0) { 574 final INode n = w.getNode(i - 1); 575 final Environment e2 = e.withPrimitive(n).withParent(w).withChild(e.osm); 576 if (left.matches(e2) && link.matches(e2.withLinkContext())) { 577 e.child = n; 578 e.index = i; 579 e.count = w.getNodesCount(); 580 e.parent = w; 581 return true; 582 } 583 } 584 } 585 } 586 } 587 } else if (ChildOrParentSelectorType.CHILD == type 588 && !link.getConditions().isEmpty() 589 && link.getConditions().get(0) instanceof OpenEndPseudoClassCondition) { 590 if (e.osm instanceof INode) { 591 e.osm.visitReferrers(new MultipolygonOpenEndFinder(e)); 592 return e.parent != null; 593 } 594 } else if (ChildOrParentSelectorType.CHILD == type) { 595 MatchingReferrerFinder collector = new MatchingReferrerFinder(e); 596 e.osm.visitReferrers(collector); 597 if (e.parent != null) 598 return true; 599 } else if (ChildOrParentSelectorType.PARENT == type) { 600 if (e.osm instanceof IWay) { 601 List<? extends INode> wayNodes = ((IWay<?>) e.osm).getNodes(); 602 for (int i = 0; i < wayNodes.size(); i++) { 603 INode n = wayNodes.get(i); 604 if (left.matches(e.withPrimitive(n)) 605 && link.matches(e.withChildAndIndexAndLinkContext(n, i, wayNodes.size()))) { 606 e.child = n; 607 e.index = i; 608 e.count = wayNodes.size(); 609 return true; 610 } 611 } 612 } else if (e.osm instanceof IRelation) { 613 List<? extends IRelationMember<?>> members = ((IRelation<?>) e.osm).getMembers(); 614 for (int i = 0; i < members.size(); i++) { 615 IPrimitive member = members.get(i).getMember(); 616 if (left.matches(e.withPrimitive(member)) 617 && link.matches(e.withChildAndIndexAndLinkContext(member, i, members.size()))) { 618 e.child = member; 619 e.index = i; 620 e.count = members.size(); 621 return true; 622 } 623 } 624 } 625 } 626 return false; 627 } 628 629 @Override 630 public Subpart getSubpart() { 631 return right.getSubpart(); 632 } 633 634 @Override 635 public Range getRange() { 636 return right.getRange(); 637 } 638 639 @Override 640 public String toString() { 641 return left.toString() + ' ' + (ChildOrParentSelectorType.PARENT == type ? '<' : '>') + link + ' ' + right; 642 } 643 } 644 645 /** 646 * Super class of {@link org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector} and 647 * {@link org.openstreetmap.josm.gui.mappaint.mapcss.Selector.LinkSelector}. 648 * @since 5841 649 */ 650 abstract class AbstractSelector implements Selector { 651 652 private final Condition[] conds; 653 654 protected AbstractSelector(List<Condition> conditions) { 655 this.conds = conditions.toArray(new Condition[0]); 656 } 657 658 /** 659 * Determines if all conditions match the given environment. 660 * @param env The environment to check 661 * @return {@code true} if all conditions apply, false otherwise. 662 */ 663 @Override 664 public boolean matches(Environment env) { 665 CheckParameterUtil.ensureParameterNotNull(env, "env"); 666 // Avoid `conds.stream().allMatch(...)` for its high heap allocations 667 for (Condition c : conds) { 668 try { 669 if (!c.applies(env)) return false; 670 } catch (RuntimeException e) { 671 Logging.log(Logging.LEVEL_ERROR, "Exception while applying condition" + c + ':', e); 672 return false; 673 } 674 } 675 return true; 676 } 677 678 @Override 679 public List<Condition> getConditions() { 680 return Arrays.asList(conds); 681 } 682 } 683 684 /** 685 * In a child selector, conditions on the link between a parent and a child object. 686 * See <a href="https://josm.openstreetmap.de/wiki/Help/Styles/MapCSSImplementation#Linkselector">wiki</a> 687 */ 688 class LinkSelector extends AbstractSelector { 689 690 public LinkSelector(List<Condition> conditions) { 691 super(conditions); 692 } 693 694 @Override 695 public boolean matches(Environment env) { 696 Utils.ensure(env.isLinkContext(), "Requires LINK context in environment, got ''{0}''", env.getContext()); 697 return super.matches(env); 698 } 699 700 @Override 701 public String getBase() { 702 throw new UnsupportedOperationException(); 703 } 704 705 @Override 706 public Subpart getSubpart() { 707 throw new UnsupportedOperationException(); 708 } 709 710 @Override 711 public Range getRange() { 712 throw new UnsupportedOperationException(); 713 } 714 715 @Override 716 public String toString() { 717 return "LinkSelector{conditions=" + getConditions() + '}'; 718 } 719 } 720 721 /** 722 * General selector. See <a href="https://josm.openstreetmap.de/wiki/Help/Styles/MapCSSImplementation#Selectors">wiki</a> 723 */ 724 class GeneralSelector extends AbstractSelector { 725 726 public final String base; 727 public final Range range; 728 public final Subpart subpart; 729 730 public GeneralSelector(String base, Range range, List<Condition> conds, Subpart subpart) { 731 super(conds); 732 this.base = checkBase(base); 733 this.range = Objects.requireNonNull(range, "range"); 734 this.subpart = subpart != null ? subpart : Subpart.DEFAULT_SUBPART; 735 } 736 737 @Override 738 public Subpart getSubpart() { 739 return subpart; 740 } 741 742 @Override 743 public Range getRange() { 744 return range; 745 } 746 747 public boolean matchesConditions(Environment e) { 748 return super.matches(e); 749 } 750 751 @Override 752 public boolean matches(Environment e) { 753 return matchesBase(e) && super.matches(e); 754 } 755 756 /** 757 * Set base and check if this is a known value. 758 * @param base value for base 759 * @return the matching String constant for a known value 760 * @throws IllegalArgumentException if value is not knwon 761 */ 762 private static String checkBase(String base) { 763 switch(base) { 764 case "*": return BASE_ANY; 765 case "node": return BASE_NODE; 766 case "way": return BASE_WAY; 767 case "relation": return BASE_RELATION; 768 case "area": return BASE_AREA; 769 case "meta": return BASE_META; 770 case "canvas": return BASE_CANVAS; 771 case "setting": return BASE_SETTING; 772 case "settings": return BASE_SETTINGS; 773 default: 774 throw new IllegalArgumentException(MessageFormat.format("Unknown MapCSS base selector {0}", base)); 775 } 776 } 777 778 @Override 779 public String getBase() { 780 return base; 781 } 782 783 public boolean matchesBase(OsmPrimitiveType type) { 784 if (BASE_ANY.equals(base)) { 785 return true; 786 } else if (OsmPrimitiveType.NODE == type) { 787 return BASE_NODE.equals(base); 788 } else if (OsmPrimitiveType.WAY == type) { 789 return BASE_WAY.equals(base) || BASE_AREA.equals(base); 790 } else if (OsmPrimitiveType.RELATION == type) { 791 return BASE_AREA.equals(base) || BASE_RELATION.equals(base) || BASE_CANVAS.equals(base); 792 } 793 return false; 794 } 795 796 public boolean matchesBase(IPrimitive p) { 797 if (!matchesBase(p.getType())) { 798 return false; 799 } else { 800 if (p instanceof IRelation) { 801 if (BASE_AREA.equals(base)) { 802 return ((IRelation<?>) p).isMultipolygon(); 803 } else if (BASE_CANVAS.equals(base)) { 804 return p.get("#canvas") != null; 805 } 806 } 807 return true; 808 } 809 } 810 811 public boolean matchesBase(Environment e) { 812 return matchesBase(e.osm); 813 } 814 815 public static Range fromLevel(int a, int b) { 816 // for input validation in Range constructor below 817 double lower = 0; 818 double upper = Double.POSITIVE_INFINITY; 819 if (b != Integer.MAX_VALUE) { 820 lower = level2scale(b + 1); 821 } 822 if (a != 0) { 823 upper = level2scale(a); 824 } 825 return new Range(lower, upper); 826 } 827 828 public static double level2scale(int lvl) { 829 if (lvl < 0) 830 throw new IllegalArgumentException("lvl must be >= 0 but is "+lvl); 831 // preliminary formula - map such that mapnik imagery tiles of the same 832 // or similar level are displayed at the given scale 833 return 2.0 * Math.PI * WGS84.a / Math.pow(2.0, lvl) / 2.56; 834 } 835 836 public static int scale2level(double scale) { 837 if (scale < 0) 838 throw new IllegalArgumentException("scale must be >= 0 but is "+scale); 839 return (int) Math.floor(Math.log(2 * Math.PI * WGS84.a / 2.56 / scale) / Math.log(2)); 840 } 841 842 @Override 843 public String toString() { 844 return base 845 + (Range.ZERO_TO_INFINITY.equals(range) ? "" : range) 846 + getConditions().stream().map(String::valueOf).collect(Collectors.joining("")) 847 + (subpart != null && subpart != Subpart.DEFAULT_SUBPART ? ("::" + subpart) : ""); 848 } 849 } 850}