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