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