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