001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.imagery;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Image;
007import java.util.ArrayList;
008import java.util.Arrays;
009import java.util.Collection;
010import java.util.Collections;
011import java.util.List;
012import java.util.Objects;
013import java.util.TreeSet;
014import java.util.regex.Matcher;
015import java.util.regex.Pattern;
016
017import javax.swing.ImageIcon;
018
019import org.openstreetmap.gui.jmapviewer.Coordinate;
020import org.openstreetmap.gui.jmapviewer.interfaces.Attributed;
021import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTileSource;
022import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource.Mapnik;
023import org.openstreetmap.josm.Main;
024import org.openstreetmap.josm.data.Bounds;
025import org.openstreetmap.josm.data.Preferences.pref;
026import org.openstreetmap.josm.io.Capabilities;
027import org.openstreetmap.josm.io.OsmApi;
028import org.openstreetmap.josm.tools.CheckParameterUtil;
029import org.openstreetmap.josm.tools.ImageProvider;
030
031/**
032 * Class that stores info about an image background layer.
033 *
034 * @author Frederik Ramm
035 */
036public class ImageryInfo implements Comparable<ImageryInfo>, Attributed {
037
038    /**
039     * Type of imagery entry.
040     */
041    public enum ImageryType {
042        /** A WMS (Web Map Service) entry. **/
043        WMS("wms"),
044        /** A TMS (Tile Map Service) entry. **/
045        TMS("tms"),
046        /** An HTML proxy (previously used for Yahoo imagery) entry. **/
047        HTML("html"),
048        /** TMS entry for Microsoft Bing. */
049        BING("bing"),
050        /** TMS entry for Russian company <a href="https://wiki.openstreetmap.org/wiki/WikiProject_Russia/kosmosnimki">ScanEx</a>. **/
051        SCANEX("scanex"),
052        /** A WMS endpoint entry only stores the WMS server info, without layer, which are chosen later by the user. **/
053        WMS_ENDPOINT("wms_endpoint");
054
055        private final String typeString;
056
057        private ImageryType(String urlString) {
058            this.typeString = urlString;
059        }
060
061        /**
062         * Returns the unique string identifying this type.
063         * @return the unique string identifying this type
064         * @since 6690
065         */
066        public final String getTypeString() {
067            return typeString;
068        }
069
070        /**
071         * Returns the imagery type from the given type string.
072         * @param s The type string
073         * @return the imagery type matching the given type string
074         */
075        public static ImageryType fromString(String s) {
076            for (ImageryType type : ImageryType.values()) {
077                if (type.getTypeString().equals(s)) {
078                    return type;
079                }
080            }
081            return null;
082        }
083    }
084
085    /**
086     * Multi-polygon bounds for imagery backgrounds.
087     * Used to display imagery coverage in preferences and to determine relevant imagery entries based on edit location.
088     */
089    public static class ImageryBounds extends Bounds {
090
091        /**
092         * Constructs a new {@code ImageryBounds} from string.
093         * @param asString The string containing the list of shapes defining this bounds
094         * @param separator The shape separator in the given string, usually a comma
095         */
096        public ImageryBounds(String asString, String separator) {
097            super(asString, separator);
098        }
099
100        private List<Shape> shapes = new ArrayList<>();
101
102        /**
103         * Adds a new shape to this bounds.
104         * @param shape The shape to add
105         */
106        public final void addShape(Shape shape) {
107            this.shapes.add(shape);
108        }
109
110        /**
111         * Sets the list of shapes defining this bounds.
112         * @param shapes The list of shapes defining this bounds.
113         */
114        public final void setShapes(List<Shape> shapes) {
115            this.shapes = shapes;
116        }
117
118        /**
119         * Returns the list of shapes defining this bounds.
120         * @return The list of shapes defining this bounds
121         */
122        public final List<Shape> getShapes() {
123            return shapes;
124        }
125
126        @Override
127        public int hashCode() {
128            final int prime = 31;
129            int result = super.hashCode();
130            result = prime * result + ((shapes == null) ? 0 : shapes.hashCode());
131            return result;
132        }
133
134        @Override
135        public boolean equals(Object obj) {
136            if (this == obj)
137                return true;
138            if (!super.equals(obj))
139                return false;
140            if (getClass() != obj.getClass())
141                return false;
142            ImageryBounds other = (ImageryBounds) obj;
143            if (shapes == null) {
144                if (other.shapes != null)
145                    return false;
146            } else if (!shapes.equals(other.shapes))
147                return false;
148            return true;
149        }
150    }
151
152    /** name of the imagery entry (gets translated by josm usually) */
153    private String name;
154    /** original name of the imagery entry in case of translation call */
155    private String origName;
156    /** id for this imagery entry, optional at the moment */
157    private String id;
158    private String url = null;
159    private boolean defaultEntry = false;
160    private String cookies = null;
161    private String eulaAcceptanceRequired= null;
162    private ImageryType imageryType = ImageryType.WMS;
163    private double pixelPerDegree = 0.0;
164    private int defaultMaxZoom = 0;
165    private int defaultMinZoom = 0;
166    private ImageryBounds bounds = null;
167    private List<String> serverProjections;
168    private String attributionText;
169    private String attributionLinkURL;
170    private String attributionImage;
171    private String attributionImageURL;
172    private String termsOfUseText;
173    private String termsOfUseURL;
174    private String countryCode = "";
175    private String icon;
176    // when adding a field, also adapt the ImageryInfo(ImageryInfo) constructor
177
178    /**
179     * Auxiliary class to save an {@link ImageryInfo} object in the preferences.
180     */
181    public static class ImageryPreferenceEntry {
182        @pref String name;
183        @pref String id;
184        @pref String type;
185        @pref String url;
186        @pref double pixel_per_eastnorth;
187        @pref String eula;
188        @pref String attribution_text;
189        @pref String attribution_url;
190        @pref String logo_image;
191        @pref String logo_url;
192        @pref String terms_of_use_text;
193        @pref String terms_of_use_url;
194        @pref String country_code = "";
195        @pref int max_zoom;
196        @pref int min_zoom;
197        @pref String cookies;
198        @pref String bounds;
199        @pref String shapes;
200        @pref String projections;
201        @pref String icon;
202
203        /**
204         * Constructs a new empty WMS {@code ImageryPreferenceEntry}.
205         */
206        public ImageryPreferenceEntry() {
207        }
208
209        /**
210         * Constructs a new {@code ImageryPreferenceEntry} from a given {@code ImageryInfo}.
211         * @param i The corresponding imagery info
212         */
213        public ImageryPreferenceEntry(ImageryInfo i) {
214            name = i.name;
215            id = i.id;
216            type = i.imageryType.getTypeString();
217            url = i.url;
218            pixel_per_eastnorth = i.pixelPerDegree;
219            eula = i.eulaAcceptanceRequired;
220            attribution_text = i.attributionText;
221            attribution_url = i.attributionLinkURL;
222            logo_image = i.attributionImage;
223            logo_url = i.attributionImageURL;
224            terms_of_use_text = i.termsOfUseText;
225            terms_of_use_url = i.termsOfUseURL;
226            country_code = i.countryCode;
227            max_zoom = i.defaultMaxZoom;
228            min_zoom = i.defaultMinZoom;
229            cookies = i.cookies;
230            icon = i.icon;
231            if (i.bounds != null) {
232                bounds = i.bounds.encodeAsString(",");
233                StringBuilder shapesString = new StringBuilder();
234                for (Shape s : i.bounds.getShapes()) {
235                    if (shapesString.length() > 0) {
236                        shapesString.append(";");
237                    }
238                    shapesString.append(s.encodeAsString(","));
239                }
240                if (shapesString.length() > 0) {
241                    shapes = shapesString.toString();
242                }
243            }
244            if (i.serverProjections != null && !i.serverProjections.isEmpty()) {
245                StringBuilder val = new StringBuilder();
246                for (String p : i.serverProjections) {
247                    if (val.length() > 0) {
248                        val.append(",");
249                    }
250                    val.append(p);
251                }
252                projections = val.toString();
253            }
254        }
255
256        @Override
257        public String toString() {
258            String s = "ImageryPreferenceEntry [name=" + name;
259            if (id != null) {
260                s += " id=" + id;
261            }
262            s += "]";
263            return s;
264        }
265    }
266
267    /**
268     * Constructs a new WMS {@code ImageryInfo}.
269     */
270    public ImageryInfo() {
271    }
272
273    /**
274     * Constructs a new WMS {@code ImageryInfo} with a given name.
275     * @param name The entry name
276     */
277    public ImageryInfo(String name) {
278        this.name=name;
279    }
280
281    /**
282     * Constructs a new WMS {@code ImageryInfo} with given name and extended URL.
283     * @param name The entry name
284     * @param url The entry extended URL
285     */
286    public ImageryInfo(String name, String url) {
287        this.name=name;
288        setExtendedUrl(url);
289    }
290
291    /**
292     * Constructs a new WMS {@code ImageryInfo} with given name, extended and EULA URLs.
293     * @param name The entry name
294     * @param url The entry URL
295     * @param eulaAcceptanceRequired The EULA URL
296     */
297    public ImageryInfo(String name, String url, String eulaAcceptanceRequired) {
298        this.name=name;
299        setExtendedUrl(url);
300        this.eulaAcceptanceRequired = eulaAcceptanceRequired;
301    }
302
303    public ImageryInfo(String name, String url, String type, String eulaAcceptanceRequired, String cookies) {
304        this.name=name;
305        setExtendedUrl(url);
306        ImageryType t = ImageryType.fromString(type);
307        this.cookies=cookies;
308        this.eulaAcceptanceRequired = eulaAcceptanceRequired;
309        if (t != null) {
310            this.imageryType = t;
311        }
312    }
313
314    /**
315     * Constructs a new {@code ImageryInfo} from an imagery preference entry.
316     * @param e The imagery preference entry
317     */
318    public ImageryInfo(ImageryPreferenceEntry e) {
319        CheckParameterUtil.ensureParameterNotNull(e.name, "name");
320        CheckParameterUtil.ensureParameterNotNull(e.url, "url");
321        name = e.name;
322        id = e.id;
323        url = e.url;
324        cookies = e.cookies;
325        eulaAcceptanceRequired = e.eula;
326        imageryType = ImageryType.fromString(e.type);
327        if (imageryType == null) throw new IllegalArgumentException("unknown type");
328        pixelPerDegree = e.pixel_per_eastnorth;
329        defaultMaxZoom = e.max_zoom;
330        defaultMinZoom = e.min_zoom;
331        if (e.bounds != null) {
332            bounds = new ImageryBounds(e.bounds, ",");
333            if (e.shapes != null) {
334                try {
335                    for (String s : e.shapes.split(";")) {
336                        bounds.addShape(new Shape(s, ","));
337                    }
338                } catch (IllegalArgumentException ex) {
339                    Main.warn(ex);
340                }
341            }
342        }
343        if (e.projections != null) {
344            serverProjections = Arrays.asList(e.projections.split(","));
345        }
346        attributionText = e.attribution_text;
347        attributionLinkURL = e.attribution_url;
348        attributionImage = e.logo_image;
349        attributionImageURL = e.logo_url;
350        termsOfUseText = e.terms_of_use_text;
351        termsOfUseURL = e.terms_of_use_url;
352        countryCode = e.country_code;
353        icon = e.icon;
354    }
355
356    /**
357     * Constructs a new {@code ImageryInfo} from an existing one.
358     * @param i The other imagery info
359     */
360    public ImageryInfo(ImageryInfo i) {
361        this.name = i.name;
362        this.id = i.id;
363        this.url = i.url;
364        this.defaultEntry = i.defaultEntry;
365        this.cookies = i.cookies;
366        this.eulaAcceptanceRequired = null;
367        this.imageryType = i.imageryType;
368        this.pixelPerDegree = i.pixelPerDegree;
369        this.defaultMaxZoom = i.defaultMaxZoom;
370        this.defaultMinZoom = i.defaultMinZoom;
371        this.bounds = i.bounds;
372        this.serverProjections = i.serverProjections;
373        this.attributionText = i.attributionText;
374        this.attributionLinkURL = i.attributionLinkURL;
375        this.attributionImage = i.attributionImage;
376        this.attributionImageURL = i.attributionImageURL;
377        this.termsOfUseText = i.termsOfUseText;
378        this.termsOfUseURL = i.termsOfUseURL;
379        this.countryCode = i.countryCode;
380        this.icon = i.icon;
381    }
382
383    @Override
384    public boolean equals(Object o) {
385        if (this == o) return true;
386        if (o == null || getClass() != o.getClass()) return false;
387
388        ImageryInfo that = (ImageryInfo) o;
389
390        if (imageryType != that.imageryType) return false;
391        if (url != null ? !url.equals(that.url) : that.url != null) return false;
392        if (name != null ? !name.equals(that.name) : that.name != null) return false;
393
394        return true;
395    }
396
397    /**
398     * Check if this object equals another ImageryInfo with respect to the properties
399     * that get written to the preference file.
400     *
401     * The field {@link #pixelPerDegree} is ignored.
402     *
403     * @param other the ImageryInfo object to compare to
404     * @return true if they are equal
405     */
406    public boolean equalsPref(ImageryInfo other) {
407        if (other == null) {
408            return false;
409        }
410        if (!Objects.equals(this.name, other.name)) {
411            return false;
412        }
413        if (!Objects.equals(this.id, other.id)) {
414            return false;
415        }
416        if (!Objects.equals(this.url, other.url)) {
417            return false;
418        }
419        if (!Objects.equals(this.cookies, other.cookies)) {
420            return false;
421        }
422        if (!Objects.equals(this.eulaAcceptanceRequired, other.eulaAcceptanceRequired)) {
423            return false;
424        }
425        if (this.imageryType != other.imageryType) {
426            return false;
427        }
428        if (this.defaultMaxZoom != other.defaultMaxZoom) {
429            return false;
430        }
431        if (this.defaultMinZoom != other.defaultMinZoom) {
432            return false;
433        }
434        if (!Objects.equals(this.bounds, other.bounds)) {
435            return false;
436        }
437        if (!Objects.equals(this.serverProjections, other.serverProjections)) {
438            return false;
439        }
440        if (!Objects.equals(this.attributionText, other.attributionText)) {
441            return false;
442        }
443        if (!Objects.equals(this.attributionLinkURL, other.attributionLinkURL)) {
444            return false;
445        }
446        if (!Objects.equals(this.attributionImage, other.attributionImage)) {
447            return false;
448        }
449        if (!Objects.equals(this.attributionImageURL, other.attributionImageURL)) {
450            return false;
451        }
452        if (!Objects.equals(this.termsOfUseText, other.termsOfUseText)) {
453            return false;
454        }
455        if (!Objects.equals(this.termsOfUseURL, other.termsOfUseURL)) {
456            return false;
457        }
458        if (!Objects.equals(this.countryCode, other.countryCode)) {
459            return false;
460        }
461        if (!Objects.equals(this.icon, other.icon)) {
462            return false;
463        }
464        return true;
465    }
466
467
468    @Override
469    public int hashCode() {
470        int result = url != null ? url.hashCode() : 0;
471        result = 31 * result + (imageryType != null ? imageryType.hashCode() : 0);
472        return result;
473    }
474
475    @Override
476    public String toString() {
477        return "ImageryInfo{" +
478                "name='" + name + '\'' +
479                ", countryCode='" + countryCode + '\'' +
480                ", url='" + url + '\'' +
481                ", imageryType=" + imageryType +
482                '}';
483    }
484
485    @Override
486    public int compareTo(ImageryInfo in) {
487        int i = countryCode.compareTo(in.countryCode);
488        if (i == 0) {
489            i = name.toLowerCase().compareTo(in.name.toLowerCase());
490        }
491        if (i == 0) {
492            i = url.compareTo(in.url);
493        }
494        if (i == 0) {
495            i = Double.compare(pixelPerDegree, in.pixelPerDegree);
496        }
497        return i;
498    }
499
500    public boolean equalsBaseValues(ImageryInfo in) {
501        return url.equals(in.url);
502    }
503
504    public void setPixelPerDegree(double ppd) {
505        this.pixelPerDegree = ppd;
506    }
507
508    /**
509     * Sets the maximum zoom level.
510     * @param defaultMaxZoom The maximum zoom level
511     */
512    public void setDefaultMaxZoom(int defaultMaxZoom) {
513        this.defaultMaxZoom = defaultMaxZoom;
514    }
515
516    /**
517     * Sets the minimum zoom level.
518     * @param defaultMinZoom The minimum zoom level
519     */
520    public void setDefaultMinZoom(int defaultMinZoom) {
521        this.defaultMinZoom = defaultMinZoom;
522    }
523
524    /**
525     * Sets the imagery polygonial bounds.
526     * @param b The imagery bounds (non-rectangular)
527     */
528    public void setBounds(ImageryBounds b) {
529        this.bounds = b;
530    }
531
532    /**
533     * Returns the imagery polygonial bounds.
534     * @return The imagery bounds (non-rectangular)
535     */
536    public ImageryBounds getBounds() {
537        return bounds;
538    }
539
540    @Override
541    public boolean requiresAttribution() {
542        return attributionText != null || attributionImage != null || termsOfUseText != null || termsOfUseURL != null;
543    }
544
545    @Override
546    public String getAttributionText(int zoom, Coordinate topLeft, Coordinate botRight) {
547        return attributionText;
548    }
549
550    @Override
551    public String getAttributionLinkURL() {
552        return attributionLinkURL;
553    }
554
555    @Override
556    public Image getAttributionImage() {
557        ImageIcon i = ImageProvider.getIfAvailable(attributionImage);
558        if (i != null) {
559            return i.getImage();
560        }
561        return null;
562    }
563
564    @Override
565    public String getAttributionImageURL() {
566        return attributionImageURL;
567    }
568
569    @Override
570    public String getTermsOfUseText() {
571        return termsOfUseText;
572    }
573
574    @Override
575    public String getTermsOfUseURL() {
576        return termsOfUseURL;
577    }
578
579    public void setAttributionText(String text) {
580        attributionText = text;
581    }
582
583    public void setAttributionImageURL(String text) {
584        attributionImageURL = text;
585    }
586
587    public void setAttributionImage(String text) {
588        attributionImage = text;
589    }
590
591    public void setAttributionLinkURL(String text) {
592        attributionLinkURL = text;
593    }
594
595    public void setTermsOfUseText(String text) {
596        termsOfUseText = text;
597    }
598
599    public void setTermsOfUseURL(String text) {
600        termsOfUseURL = text;
601    }
602
603    /**
604     * Sets the extended URL of this entry.
605     * @param url Entry extended URL containing in addition of service URL, its type and min/max zoom info
606     */
607    public void setExtendedUrl(String url) {
608        CheckParameterUtil.ensureParameterNotNull(url);
609
610        // Default imagery type is WMS
611        this.url = url;
612        this.imageryType = ImageryType.WMS;
613
614        defaultMaxZoom = 0;
615        defaultMinZoom = 0;
616        for (ImageryType type : ImageryType.values()) {
617            Matcher m = Pattern.compile(type.getTypeString()+"(?:\\[(?:(\\d+),)?(\\d+)\\])?:(.*)").matcher(url);
618            if (m.matches()) {
619                this.url = m.group(3);
620                this.imageryType = type;
621                if (m.group(2) != null) {
622                    defaultMaxZoom = Integer.valueOf(m.group(2));
623                }
624                if (m.group(1) != null) {
625                    defaultMinZoom = Integer.valueOf(m.group(1));
626                }
627                break;
628            }
629        }
630
631        if (serverProjections == null || serverProjections.isEmpty()) {
632            try {
633                serverProjections = new ArrayList<>();
634                Matcher m = Pattern.compile(".*\\{PROJ\\(([^)}]+)\\)\\}.*").matcher(url.toUpperCase());
635                if(m.matches()) {
636                    for(String p : m.group(1).split(","))
637                        serverProjections.add(p);
638                }
639            } catch (Exception e) {
640                Main.warn(e);
641            }
642        }
643    }
644
645    /**
646     * Returns the entry name.
647     * @return The entry name
648     */
649    public String getName() {
650        return this.name;
651    }
652
653    /**
654     * Returns the entry name.
655     * @return The entry name
656     * @since 6968
657     */
658    public String getOriginalName() {
659        return this.origName != null ? this.origName : this.name;
660    }
661
662    /**
663     * Sets the entry name.
664     * @param name The entry name
665     */
666    public void setName(String name) {
667        this.name = name;
668    }
669
670    /**
671     * Sets the entry name and translates it.
672     * @param name The entry name
673     * @since 6968
674     */
675    public void setTranslatedName(String name) {
676        this.name = tr(name);
677        this.origName = name;
678    }
679
680    /**
681     * Gets the entry id.
682     *
683     * Id can be null. This gets the configured id as is. Due to a user error,
684     * this may not be unique. Use {@link ImageryLayerInfo#getUniqueId} to ensure
685     * a unique value.
686     * @return the id
687     */
688    public String getId() {
689        return this.id;
690    }
691
692    /**
693     * Sets the entry id.
694     * @param id the entry id
695     */
696    public void setId(String id) {
697        this.id = id;
698    }
699
700    public void clearId() {
701        if (this.id != null) {
702            Collection<String> newAddedIds = new TreeSet<>(Main.pref.getCollection("imagery.layers.addedIds"));
703            newAddedIds.add(this.id);
704            Main.pref.putCollection("imagery.layers.addedIds", newAddedIds);
705        }
706        this.id = null;
707    }
708
709    /**
710     * Returns the entry URL.
711     * @return The entry URL
712     */
713    public String getUrl() {
714        return this.url;
715    }
716
717    /**
718     * Sets the entry URL.
719     * @param url The entry URL
720     */
721    public void setUrl(String url) {
722        this.url = url;
723    }
724
725    /**
726     * Determines if this entry is enabled by default.
727     * @return {@code true} if this entry is enabled by default, {@code false} otherwise
728     */
729    public boolean isDefaultEntry() {
730        return defaultEntry;
731    }
732
733    /**
734     * Sets the default state of this entry.
735     * @param defaultEntry {@code true} if this entry has to be enabled by default, {@code false} otherwise
736     */
737    public void setDefaultEntry(boolean defaultEntry) {
738        this.defaultEntry = defaultEntry;
739    }
740
741    public String getCookies() {
742        return this.cookies;
743    }
744
745    public double getPixelPerDegree() {
746        return this.pixelPerDegree;
747    }
748
749    /**
750     * Returns the maximum zoom level.
751     * @return The maximum zoom level
752     */
753    public int getMaxZoom() {
754        return this.defaultMaxZoom;
755    }
756
757    /**
758     * Returns the minimum zoom level.
759     * @return The minimum zoom level
760     */
761    public int getMinZoom() {
762        return this.defaultMinZoom;
763    }
764
765    /**
766     * Returns the EULA acceptance URL, if any.
767     * @return The URL to an EULA text that has to be accepted before use, or {@code null}
768     */
769    public String getEulaAcceptanceRequired() {
770        return eulaAcceptanceRequired;
771    }
772
773    /**
774     * Sets the EULA acceptance URL.
775     * @param eulaAcceptanceRequired The URL to an EULA text that has to be accepted before use
776     */
777    public void setEulaAcceptanceRequired(String eulaAcceptanceRequired) {
778        this.eulaAcceptanceRequired = eulaAcceptanceRequired;
779    }
780
781    /**
782     * Returns the ISO 3166-1-alpha-2 country code.
783     * @return The country code (2 letters)
784     */
785    public String getCountryCode() {
786        return countryCode;
787    }
788
789    /**
790     * Sets the ISO 3166-1-alpha-2 country code.
791     * @param countryCode The country code (2 letters)
792     */
793    public void setCountryCode(String countryCode) {
794        this.countryCode = countryCode;
795    }
796
797    /**
798     * Returns the entry icon.
799     * @return The entry icon
800     */
801    public String getIcon() {
802        return icon;
803    }
804
805    /**
806     * Sets the entry icon.
807     * @param icon The entry icon
808     */
809    public void setIcon(String icon) {
810        this.icon = icon;
811    }
812
813    /**
814     * Get the projections supported by the server. Only relevant for
815     * WMS-type ImageryInfo at the moment.
816     * @return null, if no projections have been specified; the list
817     * of supported projections otherwise.
818     */
819    public List<String> getServerProjections() {
820        if (serverProjections == null)
821            return Collections.emptyList();
822        return Collections.unmodifiableList(serverProjections);
823    }
824
825    public void setServerProjections(Collection<String> serverProjections) {
826        this.serverProjections = new ArrayList<>(serverProjections);
827    }
828
829    /**
830     * Returns the extended URL, containing in addition of service URL, its type and min/max zoom info.
831     * @return The extended URL
832     */
833    public String getExtendedUrl() {
834        return imageryType.getTypeString() + (defaultMaxZoom != 0
835            ? "["+(defaultMinZoom != 0 ? defaultMinZoom+",":"")+defaultMaxZoom+"]" : "") + ":" + url;
836    }
837
838    public String getToolbarName() {
839        String res = name;
840        if(pixelPerDegree != 0.0) {
841            res += "#PPD="+pixelPerDegree;
842        }
843        return res;
844    }
845
846    public String getMenuName() {
847        String res = name;
848        if(pixelPerDegree != 0.0) {
849            res += " ("+pixelPerDegree+")";
850        }
851        return res;
852    }
853
854    /**
855     * Determines if this entry requires attribution.
856     * @return {@code true} if some attribution text has to be displayed, {@code false} otherwise
857     */
858    public boolean hasAttribution() {
859        return attributionText != null;
860    }
861
862    /**
863     * Copies attribution from another {@code ImageryInfo}.
864     * @param i The other imagery info to get attribution from
865     */
866    public void copyAttribution(ImageryInfo i) {
867        this.attributionImage = i.attributionImage;
868        this.attributionImageURL = i.attributionImageURL;
869        this.attributionText = i.attributionText;
870        this.attributionLinkURL = i.attributionLinkURL;
871        this.termsOfUseText = i.termsOfUseText;
872        this.termsOfUseURL = i.termsOfUseURL;
873    }
874
875    /**
876     * Applies the attribution from this object to a tile source.
877     * @param s The tile source
878     */
879    public void setAttribution(AbstractTileSource s) {
880        if (attributionText != null) {
881            if ("osm".equals(attributionText)) {
882                s.setAttributionText(new Mapnik().getAttributionText(0, null, null));
883            } else {
884                s.setAttributionText(attributionText);
885            }
886        }
887        if (attributionLinkURL != null) {
888            if ("osm".equals(attributionLinkURL)) {
889                s.setAttributionLinkURL(new Mapnik().getAttributionLinkURL());
890            } else {
891                s.setAttributionLinkURL(attributionLinkURL);
892            }
893        }
894        if (attributionImage != null) {
895            ImageIcon i = ImageProvider.getIfAvailable(null, attributionImage);
896            if (i != null) {
897                s.setAttributionImage(i.getImage());
898            }
899        }
900        if (attributionImageURL != null) {
901            s.setAttributionImageURL(attributionImageURL);
902        }
903        if (termsOfUseText != null) {
904            s.setTermsOfUseText(termsOfUseText);
905        }
906        if (termsOfUseURL != null) {
907            if ("osm".equals(termsOfUseURL)) {
908                s.setTermsOfUseURL(new Mapnik().getTermsOfUseURL());
909            } else {
910                s.setTermsOfUseURL(termsOfUseURL);
911            }
912        }
913    }
914
915    /**
916     * Returns the imagery type.
917     * @return The imagery type
918     */
919    public ImageryType getImageryType() {
920        return imageryType;
921    }
922
923    /**
924     * Sets the imagery type.
925     * @param imageryType The imagery type
926     */
927    public void setImageryType(ImageryType imageryType) {
928        this.imageryType = imageryType;
929    }
930
931    /**
932     * Returns true if this layer's URL is matched by one of the regular
933     * expressions kept by the current OsmApi instance.
934     * @return {@code true} is this entry is blacklisted, {@code false} otherwise
935     */
936    public boolean isBlacklisted() {
937        Capabilities capabilities = OsmApi.getOsmApi().getCapabilities();
938        return capabilities != null && capabilities.isOnImageryBlacklist(this.url);
939    }
940}