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