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