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