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