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