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 &lt; x &lt;= upper" where 0 &lt;= lower &lt; 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 &gt; selector_b { ... }       // the standard notation (child selector)
132     *    relation[type=route] &gt; way { ... }    // example (all ways of a route)
133     *
134     *    selector_a &lt; selector_b { ... }       // the inverse notation (parent selector)
135     *    node[traffic_calming] &lt; 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}