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    /**
304     * Constructs a new {@code ImageryInfo} with given name, url, extended and EULA URLs.
305     * @param name The entry name
306     * @param url The entry URL
307     * @param type The entry imagery type. If null, WMS will be used as default
308     * @param eulaAcceptanceRequired The EULA URL
309     * @throws IllegalArgumentException if type refers to an unknown imagery type
310     */
311    public ImageryInfo(String name, String url, String type, String eulaAcceptanceRequired, String cookies) {
312        this.name=name;
313        setExtendedUrl(url);
314        ImageryType t = ImageryType.fromString(type);
315        this.cookies=cookies;
316        this.eulaAcceptanceRequired = eulaAcceptanceRequired;
317        if (t != null) {
318            this.imageryType = t;
319        } else if (type != null && !type.trim().isEmpty()) {
320            throw new IllegalArgumentException("unknown type: "+type);
321        }
322    }
323
324    /**
325     * Constructs a new {@code ImageryInfo} from an imagery preference entry.
326     * @param e The imagery preference entry
327     */
328    public ImageryInfo(ImageryPreferenceEntry e) {
329        CheckParameterUtil.ensureParameterNotNull(e.name, "name");
330        CheckParameterUtil.ensureParameterNotNull(e.url, "url");
331        name = e.name;
332        id = e.id;
333        url = e.url;
334        cookies = e.cookies;
335        eulaAcceptanceRequired = e.eula;
336        imageryType = ImageryType.fromString(e.type);
337        if (imageryType == null) throw new IllegalArgumentException("unknown type");
338        pixelPerDegree = e.pixel_per_eastnorth;
339        defaultMaxZoom = e.max_zoom;
340        defaultMinZoom = e.min_zoom;
341        if (e.bounds != null) {
342            bounds = new ImageryBounds(e.bounds, ",");
343            if (e.shapes != null) {
344                try {
345                    for (String s : e.shapes.split(";")) {
346                        bounds.addShape(new Shape(s, ","));
347                    }
348                } catch (IllegalArgumentException ex) {
349                    Main.warn(ex);
350                }
351            }
352        }
353        if (e.projections != null) {
354            serverProjections = Arrays.asList(e.projections.split(","));
355        }
356        attributionText = e.attribution_text;
357        attributionLinkURL = e.attribution_url;
358        attributionImage = e.logo_image;
359        attributionImageURL = e.logo_url;
360        termsOfUseText = e.terms_of_use_text;
361        termsOfUseURL = e.terms_of_use_url;
362        countryCode = e.country_code;
363        icon = e.icon;
364    }
365
366    /**
367     * Constructs a new {@code ImageryInfo} from an existing one.
368     * @param i The other imagery info
369     */
370    public ImageryInfo(ImageryInfo i) {
371        this.name = i.name;
372        this.id = i.id;
373        this.url = i.url;
374        this.defaultEntry = i.defaultEntry;
375        this.cookies = i.cookies;
376        this.eulaAcceptanceRequired = null;
377        this.imageryType = i.imageryType;
378        this.pixelPerDegree = i.pixelPerDegree;
379        this.defaultMaxZoom = i.defaultMaxZoom;
380        this.defaultMinZoom = i.defaultMinZoom;
381        this.bounds = i.bounds;
382        this.serverProjections = i.serverProjections;
383        this.attributionText = i.attributionText;
384        this.attributionLinkURL = i.attributionLinkURL;
385        this.attributionImage = i.attributionImage;
386        this.attributionImageURL = i.attributionImageURL;
387        this.termsOfUseText = i.termsOfUseText;
388        this.termsOfUseURL = i.termsOfUseURL;
389        this.countryCode = i.countryCode;
390        this.icon = i.icon;
391    }
392
393    @Override
394    public boolean equals(Object o) {
395        if (this == o) return true;
396        if (o == null || getClass() != o.getClass()) return false;
397
398        ImageryInfo that = (ImageryInfo) o;
399
400        if (imageryType != that.imageryType) return false;
401        if (url != null ? !url.equals(that.url) : that.url != null) return false;
402        if (name != null ? !name.equals(that.name) : that.name != null) return false;
403
404        return true;
405    }
406
407    /**
408     * Check if this object equals another ImageryInfo with respect to the properties
409     * that get written to the preference file.
410     *
411     * The field {@link #pixelPerDegree} is ignored.
412     *
413     * @param other the ImageryInfo object to compare to
414     * @return true if they are equal
415     */
416    public boolean equalsPref(ImageryInfo other) {
417        if (other == null) {
418            return false;
419        }
420        if (!Objects.equals(this.name, other.name)) {
421            return false;
422        }
423        if (!Objects.equals(this.id, other.id)) {
424            return false;
425        }
426        if (!Objects.equals(this.url, other.url)) {
427            return false;
428        }
429        if (!Objects.equals(this.cookies, other.cookies)) {
430            return false;
431        }
432        if (!Objects.equals(this.eulaAcceptanceRequired, other.eulaAcceptanceRequired)) {
433            return false;
434        }
435        if (this.imageryType != other.imageryType) {
436            return false;
437        }
438        if (this.defaultMaxZoom != other.defaultMaxZoom) {
439            return false;
440        }
441        if (this.defaultMinZoom != other.defaultMinZoom) {
442            return false;
443        }
444        if (!Objects.equals(this.bounds, other.bounds)) {
445            return false;
446        }
447        if (!Objects.equals(this.serverProjections, other.serverProjections)) {
448            return false;
449        }
450        if (!Objects.equals(this.attributionText, other.attributionText)) {
451            return false;
452        }
453        if (!Objects.equals(this.attributionLinkURL, other.attributionLinkURL)) {
454            return false;
455        }
456        if (!Objects.equals(this.attributionImage, other.attributionImage)) {
457            return false;
458        }
459        if (!Objects.equals(this.attributionImageURL, other.attributionImageURL)) {
460            return false;
461        }
462        if (!Objects.equals(this.termsOfUseText, other.termsOfUseText)) {
463            return false;
464        }
465        if (!Objects.equals(this.termsOfUseURL, other.termsOfUseURL)) {
466            return false;
467        }
468        if (!Objects.equals(this.countryCode, other.countryCode)) {
469            return false;
470        }
471        if (!Objects.equals(this.icon, other.icon)) {
472            return false;
473        }
474        return true;
475    }
476
477
478    @Override
479    public int hashCode() {
480        int result = url != null ? url.hashCode() : 0;
481        result = 31 * result + (imageryType != null ? imageryType.hashCode() : 0);
482        return result;
483    }
484
485    @Override
486    public String toString() {
487        return "ImageryInfo{" +
488                "name='" + name + '\'' +
489                ", countryCode='" + countryCode + '\'' +
490                ", url='" + url + '\'' +
491                ", imageryType=" + imageryType +
492                '}';
493    }
494
495    @Override
496    public int compareTo(ImageryInfo in) {
497        int i = countryCode.compareTo(in.countryCode);
498        if (i == 0) {
499            i = name.toLowerCase().compareTo(in.name.toLowerCase());
500        }
501        if (i == 0) {
502            i = url.compareTo(in.url);
503        }
504        if (i == 0) {
505            i = Double.compare(pixelPerDegree, in.pixelPerDegree);
506        }
507        return i;
508    }
509
510    public boolean equalsBaseValues(ImageryInfo in) {
511        return url.equals(in.url);
512    }
513
514    public void setPixelPerDegree(double ppd) {
515        this.pixelPerDegree = ppd;
516    }
517
518    /**
519     * Sets the maximum zoom level.
520     * @param defaultMaxZoom The maximum zoom level
521     */
522    public void setDefaultMaxZoom(int defaultMaxZoom) {
523        this.defaultMaxZoom = defaultMaxZoom;
524    }
525
526    /**
527     * Sets the minimum zoom level.
528     * @param defaultMinZoom The minimum zoom level
529     */
530    public void setDefaultMinZoom(int defaultMinZoom) {
531        this.defaultMinZoom = defaultMinZoom;
532    }
533
534    /**
535     * Sets the imagery polygonial bounds.
536     * @param b The imagery bounds (non-rectangular)
537     */
538    public void setBounds(ImageryBounds b) {
539        this.bounds = b;
540    }
541
542    /**
543     * Returns the imagery polygonial bounds.
544     * @return The imagery bounds (non-rectangular)
545     */
546    public ImageryBounds getBounds() {
547        return bounds;
548    }
549
550    @Override
551    public boolean requiresAttribution() {
552        return attributionText != null || attributionImage != null || termsOfUseText != null || termsOfUseURL != null;
553    }
554
555    @Override
556    public String getAttributionText(int zoom, Coordinate topLeft, Coordinate botRight) {
557        return attributionText;
558    }
559
560    @Override
561    public String getAttributionLinkURL() {
562        return attributionLinkURL;
563    }
564
565    @Override
566    public Image getAttributionImage() {
567        ImageIcon i = ImageProvider.getIfAvailable(attributionImage);
568        if (i != null) {
569            return i.getImage();
570        }
571        return null;
572    }
573
574    @Override
575    public String getAttributionImageURL() {
576        return attributionImageURL;
577    }
578
579    @Override
580    public String getTermsOfUseText() {
581        return termsOfUseText;
582    }
583
584    @Override
585    public String getTermsOfUseURL() {
586        return termsOfUseURL;
587    }
588
589    public void setAttributionText(String text) {
590        attributionText = text;
591    }
592
593    public void setAttributionImageURL(String text) {
594        attributionImageURL = text;
595    }
596
597    public void setAttributionImage(String text) {
598        attributionImage = text;
599    }
600
601    public void setAttributionLinkURL(String text) {
602        attributionLinkURL = text;
603    }
604
605    public void setTermsOfUseText(String text) {
606        termsOfUseText = text;
607    }
608
609    public void setTermsOfUseURL(String text) {
610        termsOfUseURL = text;
611    }
612
613    /**
614     * Sets the extended URL of this entry.
615     * @param url Entry extended URL containing in addition of service URL, its type and min/max zoom info
616     */
617    public void setExtendedUrl(String url) {
618        CheckParameterUtil.ensureParameterNotNull(url);
619
620        // Default imagery type is WMS
621        this.url = url;
622        this.imageryType = ImageryType.WMS;
623
624        defaultMaxZoom = 0;
625        defaultMinZoom = 0;
626        for (ImageryType type : ImageryType.values()) {
627            Matcher m = Pattern.compile(type.getTypeString()+"(?:\\[(?:(\\d+),)?(\\d+)\\])?:(.*)").matcher(url);
628            if (m.matches()) {
629                this.url = m.group(3);
630                this.imageryType = type;
631                if (m.group(2) != null) {
632                    defaultMaxZoom = Integer.valueOf(m.group(2));
633                }
634                if (m.group(1) != null) {
635                    defaultMinZoom = Integer.valueOf(m.group(1));
636                }
637                break;
638            }
639        }
640
641        if (serverProjections == null || serverProjections.isEmpty()) {
642            try {
643                serverProjections = new ArrayList<>();
644                Matcher m = Pattern.compile(".*\\{PROJ\\(([^)}]+)\\)\\}.*").matcher(url.toUpperCase());
645                if(m.matches()) {
646                    for(String p : m.group(1).split(","))
647                        serverProjections.add(p);
648                }
649            } catch (Exception e) {
650                Main.warn(e);
651            }
652        }
653    }
654
655    /**
656     * Returns the entry name.
657     * @return The entry name
658     */
659    public String getName() {
660        return this.name;
661    }
662
663    /**
664     * Returns the entry name.
665     * @return The entry name
666     * @since 6968
667     */
668    public String getOriginalName() {
669        return this.origName != null ? this.origName : this.name;
670    }
671
672    /**
673     * Sets the entry name.
674     * @param name The entry name
675     */
676    public void setName(String name) {
677        this.name = name;
678    }
679
680    /**
681     * Sets the entry name and translates it.
682     * @param name The entry name
683     * @since 6968
684     */
685    public void setTranslatedName(String name) {
686        this.name = tr(name);
687        this.origName = name;
688    }
689
690    /**
691     * Gets the entry id.
692     *
693     * Id can be null. This gets the configured id as is. Due to a user error,
694     * this may not be unique. Use {@link ImageryLayerInfo#getUniqueId} to ensure
695     * a unique value.
696     * @return the id
697     */
698    public String getId() {
699        return this.id;
700    }
701
702    /**
703     * Sets the entry id.
704     * @param id the entry id
705     */
706    public void setId(String id) {
707        this.id = id;
708    }
709
710    public void clearId() {
711        if (this.id != null) {
712            Collection<String> newAddedIds = new TreeSet<>(Main.pref.getCollection("imagery.layers.addedIds"));
713            newAddedIds.add(this.id);
714            Main.pref.putCollection("imagery.layers.addedIds", newAddedIds);
715        }
716        this.id = null;
717    }
718
719    /**
720     * Returns the entry URL.
721     * @return The entry URL
722     */
723    public String getUrl() {
724        return this.url;
725    }
726
727    /**
728     * Sets the entry URL.
729     * @param url The entry URL
730     */
731    public void setUrl(String url) {
732        this.url = url;
733    }
734
735    /**
736     * Determines if this entry is enabled by default.
737     * @return {@code true} if this entry is enabled by default, {@code false} otherwise
738     */
739    public boolean isDefaultEntry() {
740        return defaultEntry;
741    }
742
743    /**
744     * Sets the default state of this entry.
745     * @param defaultEntry {@code true} if this entry has to be enabled by default, {@code false} otherwise
746     */
747    public void setDefaultEntry(boolean defaultEntry) {
748        this.defaultEntry = defaultEntry;
749    }
750
751    public String getCookies() {
752        return this.cookies;
753    }
754
755    public double getPixelPerDegree() {
756        return this.pixelPerDegree;
757    }
758
759    /**
760     * Returns the maximum zoom level.
761     * @return The maximum zoom level
762     */
763    public int getMaxZoom() {
764        return this.defaultMaxZoom;
765    }
766
767    /**
768     * Returns the minimum zoom level.
769     * @return The minimum zoom level
770     */
771    public int getMinZoom() {
772        return this.defaultMinZoom;
773    }
774
775    /**
776     * Returns the EULA acceptance URL, if any.
777     * @return The URL to an EULA text that has to be accepted before use, or {@code null}
778     */
779    public String getEulaAcceptanceRequired() {
780        return eulaAcceptanceRequired;
781    }
782
783    /**
784     * Sets the EULA acceptance URL.
785     * @param eulaAcceptanceRequired The URL to an EULA text that has to be accepted before use
786     */
787    public void setEulaAcceptanceRequired(String eulaAcceptanceRequired) {
788        this.eulaAcceptanceRequired = eulaAcceptanceRequired;
789    }
790
791    /**
792     * Returns the ISO 3166-1-alpha-2 country code.
793     * @return The country code (2 letters)
794     */
795    public String getCountryCode() {
796        return countryCode;
797    }
798
799    /**
800     * Sets the ISO 3166-1-alpha-2 country code.
801     * @param countryCode The country code (2 letters)
802     */
803    public void setCountryCode(String countryCode) {
804        this.countryCode = countryCode;
805    }
806
807    /**
808     * Returns the entry icon.
809     * @return The entry icon
810     */
811    public String getIcon() {
812        return icon;
813    }
814
815    /**
816     * Sets the entry icon.
817     * @param icon The entry icon
818     */
819    public void setIcon(String icon) {
820        this.icon = icon;
821    }
822
823    /**
824     * Get the projections supported by the server. Only relevant for
825     * WMS-type ImageryInfo at the moment.
826     * @return null, if no projections have been specified; the list
827     * of supported projections otherwise.
828     */
829    public List<String> getServerProjections() {
830        if (serverProjections == null)
831            return Collections.emptyList();
832        return Collections.unmodifiableList(serverProjections);
833    }
834
835    public void setServerProjections(Collection<String> serverProjections) {
836        this.serverProjections = new ArrayList<>(serverProjections);
837    }
838
839    /**
840     * Returns the extended URL, containing in addition of service URL, its type and min/max zoom info.
841     * @return The extended URL
842     */
843    public String getExtendedUrl() {
844        return imageryType.getTypeString() + (defaultMaxZoom != 0
845            ? "["+(defaultMinZoom != 0 ? defaultMinZoom+",":"")+defaultMaxZoom+"]" : "") + ":" + url;
846    }
847
848    public String getToolbarName() {
849        String res = name;
850        if(pixelPerDegree != 0.0) {
851            res += "#PPD="+pixelPerDegree;
852        }
853        return res;
854    }
855
856    public String getMenuName() {
857        String res = name;
858        if(pixelPerDegree != 0.0) {
859            res += " ("+pixelPerDegree+")";
860        }
861        return res;
862    }
863
864    /**
865     * Determines if this entry requires attribution.
866     * @return {@code true} if some attribution text has to be displayed, {@code false} otherwise
867     */
868    public boolean hasAttribution() {
869        return attributionText != null;
870    }
871
872    /**
873     * Copies attribution from another {@code ImageryInfo}.
874     * @param i The other imagery info to get attribution from
875     */
876    public void copyAttribution(ImageryInfo i) {
877        this.attributionImage = i.attributionImage;
878        this.attributionImageURL = i.attributionImageURL;
879        this.attributionText = i.attributionText;
880        this.attributionLinkURL = i.attributionLinkURL;
881        this.termsOfUseText = i.termsOfUseText;
882        this.termsOfUseURL = i.termsOfUseURL;
883    }
884
885    /**
886     * Applies the attribution from this object to a tile source.
887     * @param s The tile source
888     */
889    public void setAttribution(AbstractTileSource s) {
890        if (attributionText != null) {
891            if ("osm".equals(attributionText)) {
892                s.setAttributionText(new Mapnik().getAttributionText(0, null, null));
893            } else {
894                s.setAttributionText(attributionText);
895            }
896        }
897        if (attributionLinkURL != null) {
898            if ("osm".equals(attributionLinkURL)) {
899                s.setAttributionLinkURL(new Mapnik().getAttributionLinkURL());
900            } else {
901                s.setAttributionLinkURL(attributionLinkURL);
902            }
903        }
904        if (attributionImage != null) {
905            ImageIcon i = ImageProvider.getIfAvailable(null, attributionImage);
906            if (i != null) {
907                s.setAttributionImage(i.getImage());
908            }
909        }
910        if (attributionImageURL != null) {
911            s.setAttributionImageURL(attributionImageURL);
912        }
913        if (termsOfUseText != null) {
914            s.setTermsOfUseText(termsOfUseText);
915        }
916        if (termsOfUseURL != null) {
917            if ("osm".equals(termsOfUseURL)) {
918                s.setTermsOfUseURL(new Mapnik().getTermsOfUseURL());
919            } else {
920                s.setTermsOfUseURL(termsOfUseURL);
921            }
922        }
923    }
924
925    /**
926     * Returns the imagery type.
927     * @return The imagery type
928     */
929    public ImageryType getImageryType() {
930        return imageryType;
931    }
932
933    /**
934     * Sets the imagery type.
935     * @param imageryType The imagery type
936     */
937    public void setImageryType(ImageryType imageryType) {
938        this.imageryType = imageryType;
939    }
940
941    /**
942     * Returns true if this layer's URL is matched by one of the regular
943     * expressions kept by the current OsmApi instance.
944     * @return {@code true} is this entry is blacklisted, {@code false} otherwise
945     */
946    public boolean isBlacklisted() {
947        Capabilities capabilities = OsmApi.getOsmApi().getCapabilities();
948        return capabilities != null && capabilities.isOnImageryBlacklist(this.url);
949    }
950}