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