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.Locale;
013import java.util.Map;
014import java.util.Objects;
015import java.util.Set;
016import java.util.TreeSet;
017import java.util.regex.Matcher;
018import java.util.regex.Pattern;
019import java.util.stream.Collectors;
020
021import javax.swing.ImageIcon;
022
023import org.openstreetmap.gui.jmapviewer.interfaces.Attributed;
024import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
025import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTileSource;
026import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource.Mapnik;
027import org.openstreetmap.gui.jmapviewer.tilesources.TileSourceInfo;
028import org.openstreetmap.josm.data.Bounds;
029import org.openstreetmap.josm.data.StructUtils;
030import org.openstreetmap.josm.data.StructUtils.StructEntry;
031import org.openstreetmap.josm.io.Capabilities;
032import org.openstreetmap.josm.io.OsmApi;
033import org.openstreetmap.josm.spi.preferences.Config;
034import org.openstreetmap.josm.spi.preferences.IPreferences;
035import org.openstreetmap.josm.tools.CheckParameterUtil;
036import org.openstreetmap.josm.tools.ImageProvider;
037import org.openstreetmap.josm.tools.LanguageInfo;
038import org.openstreetmap.josm.tools.Logging;
039import org.openstreetmap.josm.tools.MultiMap;
040import org.openstreetmap.josm.tools.Utils;
041
042/**
043 * Class that stores info about an image background layer.
044 *
045 * @author Frederik Ramm
046 */
047public class ImageryInfo extends TileSourceInfo implements Comparable<ImageryInfo>, Attributed {
048
049    /**
050     * Type of imagery entry.
051     */
052    public enum ImageryType {
053        /** A WMS (Web Map Service) entry. **/
054        WMS("wms"),
055        /** A TMS (Tile Map Service) entry. **/
056        TMS("tms"),
057        /** TMS entry for Microsoft Bing. */
058        BING("bing"),
059        /** TMS entry for Russian company <a href="https://wiki.openstreetmap.org/wiki/WikiProject_Russia/kosmosnimki">ScanEx</a>. **/
060        SCANEX("scanex"),
061        /** A WMS endpoint entry only stores the WMS server info, without layer, which are chosen later by the user. **/
062        WMS_ENDPOINT("wms_endpoint"),
063        /** WMTS stores GetCapabilities URL. Does not store any information about the layer **/
064        WMTS("wmts");
065
066
067        private final String typeString;
068
069        ImageryType(String urlString) {
070            this.typeString = urlString;
071        }
072
073        /**
074         * Returns the unique string identifying this type.
075         * @return the unique string identifying this type
076         * @since 6690
077         */
078        public final String getTypeString() {
079            return typeString;
080        }
081
082        /**
083         * Returns the imagery type from the given type string.
084         * @param s The type string
085         * @return the imagery type matching the given type string
086         */
087        public static ImageryType fromString(String s) {
088            for (ImageryType type : ImageryType.values()) {
089                if (type.getTypeString().equals(s)) {
090                    return type;
091                }
092            }
093            return null;
094        }
095    }
096
097    /**
098     * Multi-polygon bounds for imagery backgrounds.
099     * Used to display imagery coverage in preferences and to determine relevant imagery entries based on edit location.
100     */
101    public static class ImageryBounds extends Bounds {
102
103        /**
104         * Constructs a new {@code ImageryBounds} from string.
105         * @param asString The string containing the list of shapes defining this bounds
106         * @param separator The shape separator in the given string, usually a comma
107         */
108        public ImageryBounds(String asString, String separator) {
109            super(asString, separator);
110        }
111
112        private List<Shape> shapes = new ArrayList<>();
113
114        /**
115         * Adds a new shape to this bounds.
116         * @param shape The shape to add
117         */
118        public final void addShape(Shape shape) {
119            this.shapes.add(shape);
120        }
121
122        /**
123         * Sets the list of shapes defining this bounds.
124         * @param shapes The list of shapes defining this bounds.
125         */
126        public final void setShapes(List<Shape> shapes) {
127            this.shapes = shapes;
128        }
129
130        /**
131         * Returns the list of shapes defining this bounds.
132         * @return The list of shapes defining this bounds
133         */
134        public final List<Shape> getShapes() {
135            return shapes;
136        }
137
138        @Override
139        public int hashCode() {
140            return Objects.hash(super.hashCode(), shapes);
141        }
142
143        @Override
144        public boolean equals(Object o) {
145            if (this == o) return true;
146            if (o == null || getClass() != o.getClass()) return false;
147            if (!super.equals(o)) return false;
148            ImageryBounds that = (ImageryBounds) o;
149            return Objects.equals(shapes, that.shapes);
150        }
151    }
152
153    /** original name of the imagery entry in case of translation call, for multiple languages English when possible */
154    private String origName;
155    /** (original) language of the translated name entry */
156    private String langName;
157    /** whether this is a entry activated by default or not */
158    private boolean defaultEntry;
159    /** Whether this service requires a explicit EULA acceptance before it can be activated */
160    private String eulaAcceptanceRequired;
161    /** type of the imagery servics - WMS, TMS, ... */
162    private ImageryType imageryType = ImageryType.WMS;
163    private double pixelPerDegree;
164    /** maximum zoom level for TMS imagery */
165    private int defaultMaxZoom;
166    /** minimum zoom level for TMS imagery */
167    private int defaultMinZoom;
168    /** display bounds of imagery, displayed in prefs and used for automatic imagery selection */
169    private ImageryBounds bounds;
170    /** projections supported by WMS servers */
171    private List<String> serverProjections = Collections.emptyList();
172    /** description of the imagery entry, should contain notes what type of data it is */
173    private String description;
174    /** language of the description entry */
175    private String langDescription;
176    /** Text of a text attribution displayed when using the imagery */
177    private String attributionText;
178    /** Link to a reference stating the permission for OSM usage */
179    private String permissionReferenceURL;
180    /** Link behind the text attribution displayed when using the imagery */
181    private String attributionLinkURL;
182    /** Image of a graphical attribution displayed when using the imagery */
183    private String attributionImage;
184    /** Link behind the graphical attribution displayed when using the imagery */
185    private String attributionImageURL;
186    /** Text with usage terms displayed when using the imagery */
187    private String termsOfUseText;
188    /** Link behind the text with usage terms displayed when using the imagery */
189    private String termsOfUseURL;
190    /** country code of the imagery (for country specific imagery) */
191    private String countryCode = "";
192    /**
193      * creation date of the imagery (in the form YYYY-MM-DD;YYYY-MM-DD, where
194      * DD and MM as well as a second date are optional)
195      * @since 11570
196      */
197    private String date;
198    /**
199      * marked as best in other editors
200      * @since 11575
201      */
202    private boolean bestMarked;
203    /**
204      * marked as overlay
205      * @since 13536
206      */
207    private boolean overlay;
208    /**
209      * list of old IDs, only for loading, not handled anywhere else
210      * @since 13536
211      */
212    private Collection<String> oldIds;
213    /** mirrors of different type for this entry */
214    private List<ImageryInfo> mirrors;
215    /** icon used in menu */
216    private String icon;
217    /** is the geo reference correct - don't offer offset handling */
218    private boolean isGeoreferenceValid;
219    /** which layers should be activated by default on layer addition. **/
220    private Collection<DefaultLayer> defaultLayers = Collections.emptyList();
221    // when adding a field, also adapt the ImageryInfo(ImageryInfo)
222    // and ImageryInfo(ImageryPreferenceEntry) constructor, equals method, and ImageryPreferenceEntry
223
224    /**
225     * Auxiliary class to save an {@link ImageryInfo} object in the preferences.
226     */
227    public static class ImageryPreferenceEntry {
228        @StructEntry String name;
229        @StructEntry String d;
230        @StructEntry String id;
231        @StructEntry String type;
232        @StructEntry String url;
233        @StructEntry double pixel_per_eastnorth;
234        @StructEntry String eula;
235        @StructEntry String attribution_text;
236        @StructEntry String attribution_url;
237        @StructEntry String permission_reference_url;
238        @StructEntry String logo_image;
239        @StructEntry String logo_url;
240        @StructEntry String terms_of_use_text;
241        @StructEntry String terms_of_use_url;
242        @StructEntry String country_code = "";
243        @StructEntry String date;
244        @StructEntry int max_zoom;
245        @StructEntry int min_zoom;
246        @StructEntry String cookies;
247        @StructEntry String bounds;
248        @StructEntry String shapes;
249        @StructEntry String projections;
250        @StructEntry String icon;
251        @StructEntry String description;
252        @StructEntry MultiMap<String, String> noTileHeaders;
253        @StructEntry MultiMap<String, String> noTileChecksums;
254        @StructEntry int tileSize = -1;
255        @StructEntry Map<String, String> metadataHeaders;
256        @StructEntry boolean valid_georeference;
257        @StructEntry boolean bestMarked;
258        @StructEntry boolean modTileFeatures;
259        @StructEntry boolean overlay;
260        // TODO: disabled until change of layers is implemented
261        // @StructEntry String default_layers;
262
263        /**
264         * Constructs a new empty WMS {@code ImageryPreferenceEntry}.
265         */
266        public ImageryPreferenceEntry() {
267            // Do nothing
268        }
269
270        /**
271         * Constructs a new {@code ImageryPreferenceEntry} from a given {@code ImageryInfo}.
272         * @param i The corresponding imagery info
273         */
274        public ImageryPreferenceEntry(ImageryInfo i) {
275            name = i.name;
276            id = i.id;
277            type = i.imageryType.getTypeString();
278            url = i.url;
279            pixel_per_eastnorth = i.pixelPerDegree;
280            eula = i.eulaAcceptanceRequired;
281            attribution_text = i.attributionText;
282            attribution_url = i.attributionLinkURL;
283            permission_reference_url = i.permissionReferenceURL;
284            date = i.date;
285            bestMarked = i.bestMarked;
286            overlay = i.overlay;
287            logo_image = i.attributionImage;
288            logo_url = i.attributionImageURL;
289            terms_of_use_text = i.termsOfUseText;
290            terms_of_use_url = i.termsOfUseURL;
291            country_code = i.countryCode;
292            max_zoom = i.defaultMaxZoom;
293            min_zoom = i.defaultMinZoom;
294            cookies = i.cookies;
295            icon = i.icon;
296            description = i.description;
297            if (i.bounds != null) {
298                bounds = i.bounds.encodeAsString(",");
299                StringBuilder shapesString = new StringBuilder();
300                for (Shape s : i.bounds.getShapes()) {
301                    if (shapesString.length() > 0) {
302                        shapesString.append(';');
303                    }
304                    shapesString.append(s.encodeAsString(","));
305                }
306                if (shapesString.length() > 0) {
307                    shapes = shapesString.toString();
308                }
309            }
310            projections = i.serverProjections.stream().collect(Collectors.joining(","));
311            if (i.noTileHeaders != null && !i.noTileHeaders.isEmpty()) {
312                noTileHeaders = new MultiMap<>(i.noTileHeaders);
313            }
314
315            if (i.noTileChecksums != null && !i.noTileChecksums.isEmpty()) {
316                noTileChecksums = new MultiMap<>(i.noTileChecksums);
317            }
318
319            if (i.metadataHeaders != null && !i.metadataHeaders.isEmpty()) {
320                metadataHeaders = i.metadataHeaders;
321            }
322
323            tileSize = i.getTileSize();
324
325            valid_georeference = i.isGeoreferenceValid();
326            modTileFeatures = i.isModTileFeatures();
327            // TODO disabled until change of layers is implemented
328            // default_layers = i.defaultLayers.stream().collect(Collectors.joining(","));
329        }
330
331        @Override
332        public String toString() {
333            StringBuilder s = new StringBuilder("ImageryPreferenceEntry [name=").append(name);
334            if (id != null) {
335                s.append(" id=").append(id);
336            }
337            s.append(']');
338            return s.toString();
339        }
340    }
341
342    /**
343     * Constructs a new WMS {@code ImageryInfo}.
344     */
345    public ImageryInfo() {
346        super();
347    }
348
349    /**
350     * Constructs a new WMS {@code ImageryInfo} with a given name.
351     * @param name The entry name
352     */
353    public ImageryInfo(String name) {
354        super(name);
355    }
356
357    /**
358     * Constructs a new WMS {@code ImageryInfo} with given name and extended URL.
359     * @param name The entry name
360     * @param url The entry extended URL
361     */
362    public ImageryInfo(String name, String url) {
363        this(name);
364        setExtendedUrl(url);
365    }
366
367    /**
368     * Constructs a new WMS {@code ImageryInfo} with given name, extended and EULA URLs.
369     * @param name The entry name
370     * @param url The entry URL
371     * @param eulaAcceptanceRequired The EULA URL
372     */
373    public ImageryInfo(String name, String url, String eulaAcceptanceRequired) {
374        this(name);
375        setExtendedUrl(url);
376        this.eulaAcceptanceRequired = eulaAcceptanceRequired;
377    }
378
379    /**
380     * Constructs a new {@code ImageryInfo} with given name, url, extended and EULA URLs.
381     * @param name The entry name
382     * @param url The entry URL
383     * @param type The entry imagery type. If null, WMS will be used as default
384     * @param eulaAcceptanceRequired The EULA URL
385     * @param cookies The data part of HTTP cookies header in case the service requires cookies to work
386     * @throws IllegalArgumentException if type refers to an unknown imagery type
387     */
388    public ImageryInfo(String name, String url, String type, String eulaAcceptanceRequired, String cookies) {
389        this(name);
390        setExtendedUrl(url);
391        ImageryType t = ImageryType.fromString(type);
392        this.cookies = cookies;
393        this.eulaAcceptanceRequired = eulaAcceptanceRequired;
394        if (t != null) {
395            this.imageryType = t;
396        } else if (type != null && !type.isEmpty()) {
397            throw new IllegalArgumentException("unknown type: "+type);
398        }
399    }
400
401    /**
402     * Constructs a new {@code ImageryInfo} with given name, url, id, extended and EULA URLs.
403     * @param name The entry name
404     * @param url The entry URL
405     * @param type The entry imagery type. If null, WMS will be used as default
406     * @param eulaAcceptanceRequired The EULA URL
407     * @param cookies The data part of HTTP cookies header in case the service requires cookies to work
408     * @param id tile id
409     * @throws IllegalArgumentException if type refers to an unknown imagery type
410     */
411    public ImageryInfo(String name, String url, String type, String eulaAcceptanceRequired, String cookies, String id) {
412        this(name, url, type, eulaAcceptanceRequired, cookies);
413        setId(id);
414    }
415
416    /**
417     * Constructs a new {@code ImageryInfo} from an imagery preference entry.
418     * @param e The imagery preference entry
419     */
420    public ImageryInfo(ImageryPreferenceEntry e) {
421        super(e.name, e.url, e.id);
422        CheckParameterUtil.ensureParameterNotNull(e.name, "name");
423        CheckParameterUtil.ensureParameterNotNull(e.url, "url");
424        description = e.description;
425        cookies = e.cookies;
426        eulaAcceptanceRequired = e.eula;
427        imageryType = ImageryType.fromString(e.type);
428        if (imageryType == null) throw new IllegalArgumentException("unknown type");
429        pixelPerDegree = e.pixel_per_eastnorth;
430        defaultMaxZoom = e.max_zoom;
431        defaultMinZoom = e.min_zoom;
432        if (e.bounds != null) {
433            bounds = new ImageryBounds(e.bounds, ",");
434            if (e.shapes != null) {
435                try {
436                    for (String s : e.shapes.split(";")) {
437                        bounds.addShape(new Shape(s, ","));
438                    }
439                } catch (IllegalArgumentException ex) {
440                    Logging.warn(ex);
441                }
442            }
443        }
444        if (e.projections != null && !e.projections.isEmpty()) {
445            // split generates null element on empty string which gives one element Array[null]
446            serverProjections = Arrays.asList(e.projections.split(","));
447        }
448        attributionText = e.attribution_text;
449        attributionLinkURL = e.attribution_url;
450        permissionReferenceURL = e.permission_reference_url;
451        attributionImage = e.logo_image;
452        attributionImageURL = e.logo_url;
453        date = e.date;
454        bestMarked = e.bestMarked;
455        overlay = e.overlay;
456        termsOfUseText = e.terms_of_use_text;
457        termsOfUseURL = e.terms_of_use_url;
458        countryCode = e.country_code;
459        icon = e.icon;
460        if (e.noTileHeaders != null) {
461            noTileHeaders = e.noTileHeaders.toMap();
462        }
463        if (e.noTileChecksums != null) {
464            noTileChecksums = e.noTileChecksums.toMap();
465        }
466        setTileSize(e.tileSize);
467        metadataHeaders = e.metadataHeaders;
468        isGeoreferenceValid = e.valid_georeference;
469        modTileFeatures = e.modTileFeatures;
470        // TODO disabled until change of layers is implemented
471        // defaultLayers = Arrays.asList(e.default_layers.split(","));
472    }
473
474    /**
475     * Constructs a new {@code ImageryInfo} from an existing one.
476     * @param i The other imagery info
477     */
478    public ImageryInfo(ImageryInfo i) {
479        super(i.name, i.url, i.id);
480        this.noTileHeaders = i.noTileHeaders;
481        this.noTileChecksums = i.noTileChecksums;
482        this.minZoom = i.minZoom;
483        this.maxZoom = i.maxZoom;
484        this.cookies = i.cookies;
485        this.tileSize = i.tileSize;
486        this.metadataHeaders = i.metadataHeaders;
487        this.modTileFeatures = i.modTileFeatures;
488
489        this.origName = i.origName;
490        this.langName = i.langName;
491        this.defaultEntry = i.defaultEntry;
492        this.eulaAcceptanceRequired = null;
493        this.imageryType = i.imageryType;
494        this.pixelPerDegree = i.pixelPerDegree;
495        this.defaultMaxZoom = i.defaultMaxZoom;
496        this.defaultMinZoom = i.defaultMinZoom;
497        this.bounds = i.bounds;
498        this.serverProjections = i.serverProjections;
499        this.description = i.description;
500        this.langDescription = i.langDescription;
501        this.attributionText = i.attributionText;
502        this.permissionReferenceURL = i.permissionReferenceURL;
503        this.attributionLinkURL = i.attributionLinkURL;
504        this.attributionImage = i.attributionImage;
505        this.attributionImageURL = i.attributionImageURL;
506        this.termsOfUseText = i.termsOfUseText;
507        this.termsOfUseURL = i.termsOfUseURL;
508        this.countryCode = i.countryCode;
509        this.date = i.date;
510        this.bestMarked = i.bestMarked;
511        this.overlay = i.overlay;
512        // do not copy field {@code mirrors}
513        this.icon = i.icon;
514        this.isGeoreferenceValid = i.isGeoreferenceValid;
515        this.defaultLayers = i.defaultLayers;
516    }
517
518    @Override
519    public int hashCode() {
520        return Objects.hash(url, imageryType);
521    }
522
523    /**
524     * Check if this object equals another ImageryInfo with respect to the properties
525     * that get written to the preference file.
526     *
527     * The field {@link #pixelPerDegree} is ignored.
528     *
529     * @param other the ImageryInfo object to compare to
530     * @return true if they are equal
531     */
532    public boolean equalsPref(ImageryInfo other) {
533        if (other == null) {
534            return false;
535        }
536
537        // CHECKSTYLE.OFF: BooleanExpressionComplexity
538        return
539                Objects.equals(this.name, other.name) &&
540                Objects.equals(this.id, other.id) &&
541                Objects.equals(this.url, other.url) &&
542                Objects.equals(this.modTileFeatures, other.modTileFeatures) &&
543                Objects.equals(this.bestMarked, other.bestMarked) &&
544                Objects.equals(this.overlay, other.overlay) &&
545                Objects.equals(this.isGeoreferenceValid, other.isGeoreferenceValid) &&
546                Objects.equals(this.cookies, other.cookies) &&
547                Objects.equals(this.eulaAcceptanceRequired, other.eulaAcceptanceRequired) &&
548                Objects.equals(this.imageryType, other.imageryType) &&
549                Objects.equals(this.defaultMaxZoom, other.defaultMaxZoom) &&
550                Objects.equals(this.defaultMinZoom, other.defaultMinZoom) &&
551                Objects.equals(this.bounds, other.bounds) &&
552                Objects.equals(this.serverProjections, other.serverProjections) &&
553                Objects.equals(this.attributionText, other.attributionText) &&
554                Objects.equals(this.attributionLinkURL, other.attributionLinkURL) &&
555                Objects.equals(this.permissionReferenceURL, other.permissionReferenceURL) &&
556                Objects.equals(this.attributionImageURL, other.attributionImageURL) &&
557                Objects.equals(this.attributionImage, other.attributionImage) &&
558                Objects.equals(this.termsOfUseText, other.termsOfUseText) &&
559                Objects.equals(this.termsOfUseURL, other.termsOfUseURL) &&
560                Objects.equals(this.countryCode, other.countryCode) &&
561                Objects.equals(this.date, other.date) &&
562                Objects.equals(this.icon, other.icon) &&
563                Objects.equals(this.description, other.description) &&
564                Objects.equals(this.noTileHeaders, other.noTileHeaders) &&
565                Objects.equals(this.noTileChecksums, other.noTileChecksums) &&
566                Objects.equals(this.metadataHeaders, other.metadataHeaders) &&
567                Objects.equals(this.defaultLayers, other.defaultLayers);
568        // CHECKSTYLE.ON: BooleanExpressionComplexity
569    }
570
571    @Override
572    public boolean equals(Object o) {
573        if (this == o) return true;
574        if (o == null || getClass() != o.getClass()) return false;
575        ImageryInfo that = (ImageryInfo) o;
576        return imageryType == that.imageryType && Objects.equals(url, that.url);
577    }
578
579    @Override
580    public String toString() {
581        return "ImageryInfo{" +
582                "name='" + name + '\'' +
583                ", countryCode='" + countryCode + '\'' +
584                ", url='" + url + '\'' +
585                ", imageryType=" + imageryType +
586                '}';
587    }
588
589    @Override
590    public int compareTo(ImageryInfo in) {
591        int i = countryCode.compareTo(in.countryCode);
592        if (i == 0) {
593            i = name.toLowerCase(Locale.ENGLISH).compareTo(in.name.toLowerCase(Locale.ENGLISH));
594        }
595        if (i == 0) {
596            i = url.compareTo(in.url);
597        }
598        if (i == 0) {
599            i = Double.compare(pixelPerDegree, in.pixelPerDegree);
600        }
601        return i;
602    }
603
604    /**
605     * Determines if URL is equal to given imagery info.
606     * @param in imagery info
607     * @return {@code true} if URL is equal to given imagery info
608     */
609    public boolean equalsBaseValues(ImageryInfo in) {
610        return url.equals(in.url);
611    }
612
613    /**
614     * Sets the pixel per degree value.
615     * @param ppd The ppd value
616     * @see #getPixelPerDegree()
617     */
618    public void setPixelPerDegree(double ppd) {
619        this.pixelPerDegree = ppd;
620    }
621
622    /**
623     * Sets the maximum zoom level.
624     * @param defaultMaxZoom The maximum zoom level
625     */
626    public void setDefaultMaxZoom(int defaultMaxZoom) {
627        this.defaultMaxZoom = defaultMaxZoom;
628    }
629
630    /**
631     * Sets the minimum zoom level.
632     * @param defaultMinZoom The minimum zoom level
633     */
634    public void setDefaultMinZoom(int defaultMinZoom) {
635        this.defaultMinZoom = defaultMinZoom;
636    }
637
638    /**
639     * Sets the imagery polygonial bounds.
640     * @param b The imagery bounds (non-rectangular)
641     */
642    public void setBounds(ImageryBounds b) {
643        this.bounds = b;
644    }
645
646    /**
647     * Returns the imagery polygonial bounds.
648     * @return The imagery bounds (non-rectangular)
649     */
650    public ImageryBounds getBounds() {
651        return bounds;
652    }
653
654    @Override
655    public boolean requiresAttribution() {
656        return attributionText != null || attributionLinkURL != null || attributionImage != null
657                || termsOfUseText != null || termsOfUseURL != null;
658    }
659
660    @Override
661    public String getAttributionText(int zoom, ICoordinate topLeft, ICoordinate botRight) {
662        return attributionText;
663    }
664
665    @Override
666    public String getAttributionLinkURL() {
667        return attributionLinkURL;
668    }
669
670    /**
671     * Return the permission reference URL.
672     * @return The url
673     * @see #setPermissionReferenceURL
674     * @since 11975
675     */
676    public String getPermissionReferenceURL() {
677        return permissionReferenceURL;
678    }
679
680    @Override
681    public Image getAttributionImage() {
682        ImageIcon i = ImageProvider.getIfAvailable(attributionImage);
683        if (i != null) {
684            return i.getImage();
685        }
686        return null;
687    }
688
689    /**
690     * Return the raw attribution logo information (an URL to the image).
691     * @return The url text
692     * @since 12257
693     */
694    public String getAttributionImageRaw() {
695        return attributionImage;
696    }
697
698    @Override
699    public String getAttributionImageURL() {
700        return attributionImageURL;
701    }
702
703    @Override
704    public String getTermsOfUseText() {
705        return termsOfUseText;
706    }
707
708    @Override
709    public String getTermsOfUseURL() {
710        return termsOfUseURL;
711    }
712
713    /**
714     * Set the attribution text
715     * @param text The text
716     * @see #getAttributionText(int, ICoordinate, ICoordinate)
717     */
718    public void setAttributionText(String text) {
719        attributionText = text;
720    }
721
722    /**
723     * Set the attribution image
724     * @param url The url of the image.
725     * @see #getAttributionImageURL()
726     */
727    public void setAttributionImageURL(String url) {
728        attributionImageURL = url;
729    }
730
731    /**
732     * Set the image for the attribution
733     * @param res The image resource
734     * @see #getAttributionImage()
735     */
736    public void setAttributionImage(String res) {
737        attributionImage = res;
738    }
739
740    /**
741     * Sets the URL the attribution should link to.
742     * @param url The url.
743     * @see #getAttributionLinkURL()
744     */
745    public void setAttributionLinkURL(String url) {
746        attributionLinkURL = url;
747    }
748
749    /**
750     * Sets the permission reference URL.
751     * @param url The url.
752     * @see #getPermissionReferenceURL()
753     * @since 11975
754     */
755    public void setPermissionReferenceURL(String url) {
756        permissionReferenceURL = url;
757    }
758
759    /**
760     * Sets the text to display to the user as terms of use.
761     * @param text The text
762     * @see #getTermsOfUseText()
763     */
764    public void setTermsOfUseText(String text) {
765        termsOfUseText = text;
766    }
767
768    /**
769     * Sets a url that links to the terms of use text.
770     * @param text The url.
771     * @see #getTermsOfUseURL()
772     */
773    public void setTermsOfUseURL(String text) {
774        termsOfUseURL = text;
775    }
776
777    /**
778     * Sets the extended URL of this entry.
779     * @param url Entry extended URL containing in addition of service URL, its type and min/max zoom info
780     */
781    public void setExtendedUrl(String url) {
782        CheckParameterUtil.ensureParameterNotNull(url);
783
784        // Default imagery type is WMS
785        this.url = url;
786        this.imageryType = ImageryType.WMS;
787
788        defaultMaxZoom = 0;
789        defaultMinZoom = 0;
790        for (ImageryType type : ImageryType.values()) {
791            Matcher m = Pattern.compile(type.getTypeString()+"(?:\\[(?:(\\d+)[,-])?(\\d+)\\])?:(.*)").matcher(url);
792            if (m.matches()) {
793                this.url = m.group(3);
794                this.imageryType = type;
795                if (m.group(2) != null) {
796                    defaultMaxZoom = Integer.parseInt(m.group(2));
797                }
798                if (m.group(1) != null) {
799                    defaultMinZoom = Integer.parseInt(m.group(1));
800                }
801                break;
802            }
803        }
804
805        if (serverProjections.isEmpty()) {
806            serverProjections = new ArrayList<>();
807            Matcher m = Pattern.compile(".*\\{PROJ\\(([^)}]+)\\)\\}.*").matcher(url.toUpperCase(Locale.ENGLISH));
808            if (m.matches()) {
809                for (String p : m.group(1).split(",")) {
810                    serverProjections.add(p);
811                }
812            }
813        }
814    }
815
816    /**
817     * Returns the entry name.
818     * @return The entry name
819     * @since 6968
820     */
821    public String getOriginalName() {
822        return this.origName != null ? this.origName : this.name;
823    }
824
825    /**
826     * Sets the entry name and handle translation.
827     * @param language The used language
828     * @param name The entry name
829     * @since 8091
830     */
831    public void setName(String language, String name) {
832        boolean isdefault = LanguageInfo.getJOSMLocaleCode(null).equals(language);
833        if (LanguageInfo.isBetterLanguage(langName, language)) {
834            this.name = isdefault ? tr(name) : name;
835            this.langName = language;
836        }
837        if (origName == null || isdefault) {
838            this.origName = name;
839        }
840    }
841
842    /**
843     * Store the id of this info to the preferences and clear it afterwards.
844     */
845    public void clearId() {
846        if (this.id != null) {
847            Collection<String> newAddedIds = new TreeSet<>(Config.getPref().getList("imagery.layers.addedIds"));
848            newAddedIds.add(this.id);
849            Config.getPref().putList("imagery.layers.addedIds", new ArrayList<>(newAddedIds));
850        }
851        setId(null);
852    }
853
854    /**
855     * Determines if this entry is enabled by default.
856     * @return {@code true} if this entry is enabled by default, {@code false} otherwise
857     */
858    public boolean isDefaultEntry() {
859        return defaultEntry;
860    }
861
862    /**
863     * Sets the default state of this entry.
864     * @param defaultEntry {@code true} if this entry has to be enabled by default, {@code false} otherwise
865     */
866    public void setDefaultEntry(boolean defaultEntry) {
867        this.defaultEntry = defaultEntry;
868    }
869
870    /**
871     * Gets the pixel per degree value
872     * @return The ppd value.
873     */
874    public double getPixelPerDegree() {
875        return this.pixelPerDegree;
876    }
877
878    /**
879     * Returns the maximum zoom level.
880     * @return The maximum zoom level
881     */
882    @Override
883    public int getMaxZoom() {
884        return this.defaultMaxZoom;
885    }
886
887    /**
888     * Returns the minimum zoom level.
889     * @return The minimum zoom level
890     */
891    @Override
892    public int getMinZoom() {
893        return this.defaultMinZoom;
894    }
895
896    /**
897     * Returns the description text when existing.
898     * @return The description
899     * @since 8065
900     */
901    public String getDescription() {
902        return this.description;
903    }
904
905    /**
906     * Sets the description text when existing.
907     * @param language The used language
908     * @param description the imagery description text
909     * @since 8091
910     */
911    public void setDescription(String language, String description) {
912        boolean isdefault = LanguageInfo.getJOSMLocaleCode(null).equals(language);
913        if (LanguageInfo.isBetterLanguage(langDescription, language)) {
914            this.description = isdefault ? tr(description) : description;
915            this.langDescription = language;
916        }
917    }
918
919    /**
920     * Return the sorted list of activated Imagery IDs.
921     * @return sorted list of activated Imagery IDs
922     * @since 13536
923     */
924    public static Collection<String> getActiveIds() {
925        ArrayList<String> ids = new ArrayList<>();
926        IPreferences pref = Config.getPref();
927        if (pref != null) {
928            List<ImageryPreferenceEntry> entries = StructUtils.getListOfStructs(
929                pref, "imagery.entries", null, ImageryPreferenceEntry.class);
930            if (entries != null) {
931                for (ImageryPreferenceEntry prefEntry : entries) {
932                    if (prefEntry.id != null && !prefEntry.id.isEmpty())
933                        ids.add(prefEntry.id);
934                }
935                Collections.sort(ids);
936            }
937        }
938        return ids;
939    }
940
941    /**
942     * Returns a tool tip text for display.
943     * @return The text
944     * @since 8065
945     */
946    public String getToolTipText() {
947        StringBuilder res = new StringBuilder(getName());
948        boolean html = false;
949        String dateStr = getDate();
950        if (dateStr != null && !dateStr.isEmpty()) {
951            res.append("<br>").append(tr("Date of imagery: {0}", dateStr));
952            html = true;
953        }
954        if (bestMarked) {
955            res.append("<br>").append(tr("This imagery is marked as best in this region in other editors."));
956            html = true;
957        }
958        if (overlay) {
959            res.append("<br>").append(tr("This imagery is an overlay."));
960            html = true;
961        }
962        String desc = getDescription();
963        if (desc != null && !desc.isEmpty()) {
964            res.append("<br>").append(Utils.escapeReservedCharactersHTML(desc));
965            html = true;
966        }
967        if (html) {
968            res.insert(0, "<html>").append("</html>");
969        }
970        return res.toString();
971    }
972
973    /**
974     * Returns the EULA acceptance URL, if any.
975     * @return The URL to an EULA text that has to be accepted before use, or {@code null}
976     */
977    public String getEulaAcceptanceRequired() {
978        return eulaAcceptanceRequired;
979    }
980
981    /**
982     * Sets the EULA acceptance URL.
983     * @param eulaAcceptanceRequired The URL to an EULA text that has to be accepted before use
984     */
985    public void setEulaAcceptanceRequired(String eulaAcceptanceRequired) {
986        this.eulaAcceptanceRequired = eulaAcceptanceRequired;
987    }
988
989    /**
990     * Returns the ISO 3166-1-alpha-2 country code.
991     * @return The country code (2 letters)
992     */
993    public String getCountryCode() {
994        return countryCode;
995    }
996
997    /**
998     * Sets the ISO 3166-1-alpha-2 country code.
999     * @param countryCode The country code (2 letters)
1000     */
1001    public void setCountryCode(String countryCode) {
1002        this.countryCode = countryCode;
1003    }
1004
1005    /**
1006     * Returns the date information.
1007     * @return The date (in the form YYYY-MM-DD;YYYY-MM-DD, where
1008     * DD and MM as well as a second date are optional)
1009     * @since 11570
1010     */
1011    public String getDate() {
1012        return date;
1013    }
1014
1015    /**
1016     * Sets the date information.
1017     * @param date The date information
1018     * @since 11570
1019     */
1020    public void setDate(String date) {
1021        this.date = date;
1022    }
1023
1024    /**
1025     * Returns the entry icon.
1026     * @return The entry icon
1027     */
1028    public String getIcon() {
1029        return icon;
1030    }
1031
1032    /**
1033     * Sets the entry icon.
1034     * @param icon The entry icon
1035     */
1036    public void setIcon(String icon) {
1037        this.icon = icon;
1038    }
1039
1040    /**
1041     * Get the projections supported by the server. Only relevant for
1042     * WMS-type ImageryInfo at the moment.
1043     * @return null, if no projections have been specified; the list
1044     * of supported projections otherwise.
1045     */
1046    public List<String> getServerProjections() {
1047        return Collections.unmodifiableList(serverProjections);
1048    }
1049
1050    /**
1051     * Sets the list of collections the server supports
1052     * @param serverProjections The list of supported projections
1053     */
1054    public void setServerProjections(Collection<String> serverProjections) {
1055        CheckParameterUtil.ensureParameterNotNull(serverProjections, "serverProjections");
1056        this.serverProjections = new ArrayList<>(serverProjections);
1057    }
1058
1059    /**
1060     * Returns the extended URL, containing in addition of service URL, its type and min/max zoom info.
1061     * @return The extended URL
1062     */
1063    public String getExtendedUrl() {
1064        return imageryType.getTypeString() + (defaultMaxZoom != 0
1065            ? ('['+(defaultMinZoom != 0 ? (Integer.toString(defaultMinZoom) + ',') : "")+defaultMaxZoom+']') : "") + ':' + url;
1066    }
1067
1068    /**
1069     * Gets a unique toolbar key to store this layer as toolbar item
1070     * @return The kay.
1071     */
1072    public String getToolbarName() {
1073        String res = name;
1074        if (pixelPerDegree != 0) {
1075            res += "#PPD="+pixelPerDegree;
1076        }
1077        return res;
1078    }
1079
1080    /**
1081     * Gets the name that should be displayed in the menu to add this imagery layer.
1082     * @return The text.
1083     */
1084    public String getMenuName() {
1085        String res = name;
1086        if (pixelPerDegree != 0) {
1087            res += " ("+pixelPerDegree+')';
1088        }
1089        return res;
1090    }
1091
1092    /**
1093     * Determines if this entry requires attribution.
1094     * @return {@code true} if some attribution text has to be displayed, {@code false} otherwise
1095     */
1096    public boolean hasAttribution() {
1097        return attributionText != null;
1098    }
1099
1100    /**
1101     * Copies attribution from another {@code ImageryInfo}.
1102     * @param i The other imagery info to get attribution from
1103     */
1104    public void copyAttribution(ImageryInfo i) {
1105        this.attributionImage = i.attributionImage;
1106        this.attributionImageURL = i.attributionImageURL;
1107        this.attributionText = i.attributionText;
1108        this.attributionLinkURL = i.attributionLinkURL;
1109        this.termsOfUseText = i.termsOfUseText;
1110        this.termsOfUseURL = i.termsOfUseURL;
1111    }
1112
1113    /**
1114     * Applies the attribution from this object to a tile source.
1115     * @param s The tile source
1116     */
1117    public void setAttribution(AbstractTileSource s) {
1118        if (attributionText != null) {
1119            if ("osm".equals(attributionText)) {
1120                s.setAttributionText(new Mapnik().getAttributionText(0, null, null));
1121            } else {
1122                s.setAttributionText(attributionText);
1123            }
1124        }
1125        if (attributionLinkURL != null) {
1126            if ("osm".equals(attributionLinkURL)) {
1127                s.setAttributionLinkURL(new Mapnik().getAttributionLinkURL());
1128            } else {
1129                s.setAttributionLinkURL(attributionLinkURL);
1130            }
1131        }
1132        if (attributionImage != null) {
1133            ImageIcon i = ImageProvider.getIfAvailable(null, attributionImage);
1134            if (i != null) {
1135                s.setAttributionImage(i.getImage());
1136            }
1137        }
1138        if (attributionImageURL != null) {
1139            s.setAttributionImageURL(attributionImageURL);
1140        }
1141        if (termsOfUseText != null) {
1142            s.setTermsOfUseText(termsOfUseText);
1143        }
1144        if (termsOfUseURL != null) {
1145            if ("osm".equals(termsOfUseURL)) {
1146                s.setTermsOfUseURL(new Mapnik().getTermsOfUseURL());
1147            } else {
1148                s.setTermsOfUseURL(termsOfUseURL);
1149            }
1150        }
1151    }
1152
1153    /**
1154     * Returns the imagery type.
1155     * @return The imagery type
1156     */
1157    public ImageryType getImageryType() {
1158        return imageryType;
1159    }
1160
1161    /**
1162     * Sets the imagery type.
1163     * @param imageryType The imagery type
1164     */
1165    public void setImageryType(ImageryType imageryType) {
1166        this.imageryType = imageryType;
1167    }
1168
1169    /**
1170     * Returns true if this layer's URL is matched by one of the regular
1171     * expressions kept by the current OsmApi instance.
1172     * @return {@code true} is this entry is blacklisted, {@code false} otherwise
1173     */
1174    public boolean isBlacklisted() {
1175        Capabilities capabilities = OsmApi.getOsmApi().getCapabilities();
1176        return capabilities != null && capabilities.isOnImageryBlacklist(this.url);
1177    }
1178
1179    /**
1180     * Sets the map of &lt;header name, header value&gt; that if any of this header
1181     * will be returned, then this tile will be treated as "no tile at this zoom level"
1182     *
1183     * @param noTileHeaders Map of &lt;header name, header value&gt; which will be treated as "no tile at this zoom level"
1184     * @since 9613
1185     */
1186    public void setNoTileHeaders(MultiMap<String, String> noTileHeaders) {
1187       if (noTileHeaders == null || noTileHeaders.isEmpty()) {
1188           this.noTileHeaders = null;
1189       } else {
1190            this.noTileHeaders = noTileHeaders.toMap();
1191       }
1192    }
1193
1194    @Override
1195    public Map<String, Set<String>> getNoTileHeaders() {
1196        return noTileHeaders;
1197    }
1198
1199    /**
1200     * Sets the map of &lt;checksum type, checksum value&gt; that if any tile with that checksum
1201     * will be returned, then this tile will be treated as "no tile at this zoom level"
1202     *
1203     * @param noTileChecksums Map of &lt;checksum type, checksum value&gt; which will be treated as "no tile at this zoom level"
1204     * @since 9613
1205     */
1206    public void setNoTileChecksums(MultiMap<String, String> noTileChecksums) {
1207        if (noTileChecksums == null || noTileChecksums.isEmpty()) {
1208            this.noTileChecksums = null;
1209        } else {
1210            this.noTileChecksums = noTileChecksums.toMap();
1211        }
1212    }
1213
1214    @Override
1215    public Map<String, Set<String>> getNoTileChecksums() {
1216        return noTileChecksums;
1217    }
1218
1219    /**
1220     * Returns the map of &lt;header name, metadata key&gt; indicating, which HTTP headers should
1221     * be moved to metadata
1222     *
1223     * @param metadataHeaders map of &lt;header name, metadata key&gt; indicating, which HTTP headers should be moved to metadata
1224     * @since 8418
1225     */
1226    public void setMetadataHeaders(Map<String, String> metadataHeaders) {
1227        if (metadataHeaders == null || metadataHeaders.isEmpty()) {
1228            this.metadataHeaders = null;
1229        } else {
1230            this.metadataHeaders = metadataHeaders;
1231        }
1232    }
1233
1234    /**
1235     * Gets the flag if the georeference is valid.
1236     * @return <code>true</code> if it is valid.
1237     */
1238    public boolean isGeoreferenceValid() {
1239        return isGeoreferenceValid;
1240    }
1241
1242    /**
1243     * Sets an indicator that the georeference is valid
1244     * @param isGeoreferenceValid <code>true</code> if it is marked as valid.
1245     */
1246    public void setGeoreferenceValid(boolean isGeoreferenceValid) {
1247        this.isGeoreferenceValid = isGeoreferenceValid;
1248    }
1249
1250    /**
1251     * Returns the status of "best" marked status in other editors.
1252     * @return <code>true</code> if it is marked as best.
1253     * @since 11575
1254     */
1255    public boolean isBestMarked() {
1256        return bestMarked;
1257    }
1258
1259    /**
1260     * Returns the overlay indication.
1261     * @return <code>true</code> if it is an overlay.
1262     * @since 13536
1263     */
1264    public boolean isOverlay() {
1265        return overlay;
1266    }
1267
1268    /**
1269     * Sets an indicator that in other editors it is marked as best imagery
1270     * @param bestMarked <code>true</code> if it is marked as best in other editors.
1271     * @since 11575
1272     */
1273    public void setBestMarked(boolean bestMarked) {
1274        this.bestMarked = bestMarked;
1275    }
1276
1277    /**
1278     * Sets overlay indication
1279     * @param overlay <code>true</code> if it is an overlay.
1280     * @since 13536
1281     */
1282    public void setOverlay(boolean overlay) {
1283        this.overlay = overlay;
1284    }
1285
1286    /**
1287     * Adds an old Id.
1288     *
1289     * @param id the Id to be added
1290     * @since 13536
1291     */
1292    public void addOldId(String id) {
1293       if (oldIds == null) {
1294           oldIds = new ArrayList<>();
1295       }
1296       oldIds.add(id);
1297    }
1298
1299    /**
1300     * Get old Ids.
1301     *
1302     * @return collection of ids
1303     * @since 13536
1304     */
1305    public Collection<String> getOldIds() {
1306        return oldIds;
1307    }
1308
1309    /**
1310     * Adds a mirror entry. Mirror entries are completed with the data from the master entry
1311     * and only describe another method to access identical data.
1312     *
1313     * @param entry the mirror to be added
1314     * @since 9658
1315     */
1316    public void addMirror(ImageryInfo entry) {
1317       if (mirrors == null) {
1318           mirrors = new ArrayList<>();
1319       }
1320       mirrors.add(entry);
1321    }
1322
1323    /**
1324     * Returns the mirror entries. Entries are completed with master entry data.
1325     *
1326     * @return the list of mirrors
1327     * @since 9658
1328     */
1329    public List<ImageryInfo> getMirrors() {
1330       List<ImageryInfo> l = new ArrayList<>();
1331       if (mirrors != null) {
1332           int num = 1;
1333           for (ImageryInfo i : mirrors) {
1334               ImageryInfo n = new ImageryInfo(this);
1335               if (i.defaultMaxZoom != 0) {
1336                   n.defaultMaxZoom = i.defaultMaxZoom;
1337               }
1338               if (i.defaultMinZoom != 0) {
1339                   n.defaultMinZoom = i.defaultMinZoom;
1340               }
1341               n.setServerProjections(i.getServerProjections());
1342               n.url = i.url;
1343               n.imageryType = i.imageryType;
1344               if (i.getTileSize() != 0) {
1345                   n.setTileSize(i.getTileSize());
1346               }
1347               if (n.id != null) {
1348                   n.id = n.id + "_mirror"+num;
1349               }
1350               if (num > 1) {
1351                   n.name = tr("{0} mirror server {1}", n.name, num);
1352                   if (n.origName != null) {
1353                       n.origName += " mirror server " + num;
1354                   }
1355               } else {
1356                   n.name = tr("{0} mirror server", n.name);
1357                   if (n.origName != null) {
1358                       n.origName += " mirror server";
1359                   }
1360               }
1361               l.add(n);
1362               ++num;
1363           }
1364       }
1365       return l;
1366    }
1367
1368    /**
1369     * Returns default layers that should be shown for this Imagery (if at all supported by imagery provider)
1370     * If no layer is set to default and there is more than one imagery available, then user will be asked to choose the layer
1371     * to work on
1372     * @return Collection of the layer names
1373     */
1374    public Collection<DefaultLayer> getDefaultLayers() {
1375        return defaultLayers;
1376    }
1377
1378    /**
1379     * Sets the default layers that user will work with
1380     * @param layers set the list of default layers
1381     */
1382    public void setDefaultLayers(Collection<DefaultLayer> layers) {
1383        if (ImageryType.WMTS.equals(this.imageryType)) {
1384            CheckParameterUtil.ensureThat(layers == null ||
1385                    layers.isEmpty() ||
1386                    layers.iterator().next() instanceof WMTSDefaultLayer, "Incorrect default layer");
1387        }
1388        this.defaultLayers = layers;
1389    }
1390}