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