001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.imagery;
003
004import java.awt.Image;
005import java.util.ArrayList;
006import java.util.Arrays;
007import java.util.Collection;
008import java.util.Collections;
009import java.util.List;
010import java.util.regex.Matcher;
011import java.util.regex.Pattern;
012
013import javax.swing.ImageIcon;
014
015import org.openstreetmap.gui.jmapviewer.Coordinate;
016import org.openstreetmap.gui.jmapviewer.interfaces.Attributed;
017import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTileSource;
018import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource.Mapnik;
019import org.openstreetmap.josm.Main;
020import org.openstreetmap.josm.data.Bounds;
021import org.openstreetmap.josm.data.Preferences.pref;
022import org.openstreetmap.josm.io.OsmApi;
023import org.openstreetmap.josm.tools.CheckParameterUtil;
024import org.openstreetmap.josm.tools.ImageProvider;
025
026/**
027 * Class that stores info about an image background layer.
028 *
029 * @author Frederik Ramm <frederik@remote.org>
030 */
031public class ImageryInfo implements Comparable<ImageryInfo>, Attributed {
032
033    public enum ImageryType {
034        WMS("wms"),
035        TMS("tms"),
036        HTML("html"),
037        BING("bing"),
038        SCANEX("scanex"),
039        WMS_ENDPOINT("wms_endpoint");
040
041        private String urlString;
042
043        ImageryType(String urlString) {
044            this.urlString = urlString;
045        }
046
047        public String getUrlString() {
048            return urlString;
049        }
050
051        public static ImageryType fromUrlString(String s) {
052            for (ImageryType type : ImageryType.values()) {
053                if (type.getUrlString().equals(s)) {
054                    return type;
055                }
056            }
057            return null;
058        }
059    }
060
061    public static class ImageryBounds extends Bounds {
062        public ImageryBounds(String asString, String separator) {
063            super(asString, separator);
064        }
065
066        private List<Shape> shapes = new ArrayList<Shape>();
067
068        public void addShape(Shape shape) {
069            this.shapes.add(shape);
070        }
071
072        public void setShapes(List<Shape> shapes) {
073            this.shapes = shapes;
074        }
075
076        public List<Shape> getShapes() {
077            return shapes;
078        }
079
080        @Override
081        public int hashCode() {
082            final int prime = 31;
083            int result = super.hashCode();
084            result = prime * result + ((shapes == null) ? 0 : shapes.hashCode());
085            return result;
086        }
087
088        @Override
089        public boolean equals(Object obj) {
090            if (this == obj)
091                return true;
092            if (!super.equals(obj))
093                return false;
094            if (getClass() != obj.getClass())
095                return false;
096            ImageryBounds other = (ImageryBounds) obj;
097            if (shapes == null) {
098                if (other.shapes != null)
099                    return false;
100            } else if (!shapes.equals(other.shapes))
101                return false;
102            return true;
103        }
104    }
105
106    private String name;
107    private String url = null;
108    private boolean defaultEntry = false;
109    private String cookies = null;
110    private String eulaAcceptanceRequired= null;
111    private ImageryType imageryType = ImageryType.WMS;
112    private double pixelPerDegree = 0.0;
113    private int defaultMaxZoom = 0;
114    private int defaultMinZoom = 0;
115    private ImageryBounds bounds = null;
116    private List<String> serverProjections;
117    private String attributionText;
118    private String attributionLinkURL;
119    private String attributionImage;
120    private String attributionImageURL;
121    private String termsOfUseText;
122    private String termsOfUseURL;
123    private String countryCode = "";
124    private String icon;
125    // when adding a field, also adapt the ImageryInfo(ImageryInfo) constructor
126
127    /** auxiliary class to save an ImageryInfo object in the preferences */
128    public static class ImageryPreferenceEntry {
129        @pref String name;
130        @pref String type;
131        @pref String url;
132        @pref double pixel_per_eastnorth;
133        @pref String eula;
134        @pref String attribution_text;
135        @pref String attribution_url;
136        @pref String logo_image;
137        @pref String logo_url;
138        @pref String terms_of_use_text;
139        @pref String terms_of_use_url;
140        @pref String country_code = "";
141        @pref int max_zoom;
142        @pref int min_zoom;
143        @pref String cookies;
144        @pref String bounds;
145        @pref String shapes;
146        @pref String projections;
147        @pref String icon;
148
149        /**
150         * Constructs a new {@code ImageryPreferenceEntry}.
151         */
152        public ImageryPreferenceEntry() {
153        }
154
155        public ImageryPreferenceEntry(ImageryInfo i) {
156            name = i.name;
157            type = i.imageryType.getUrlString();
158            url = i.url;
159            pixel_per_eastnorth = i.pixelPerDegree;
160            eula = i.eulaAcceptanceRequired;
161            attribution_text = i.attributionText;
162            attribution_url = i.attributionLinkURL;
163            logo_image = i.attributionImage;
164            logo_url = i.attributionImageURL;
165            terms_of_use_text = i.termsOfUseText;
166            terms_of_use_url = i.termsOfUseURL;
167            country_code = i.countryCode;
168            max_zoom = i.defaultMaxZoom;
169            min_zoom = i.defaultMinZoom;
170            cookies = i.cookies;
171            icon = i.icon;
172            if (i.bounds != null) {
173                bounds = i.bounds.encodeAsString(",");
174                StringBuilder shapesString = new StringBuilder();
175                for (Shape s : i.bounds.getShapes()) {
176                    if (shapesString.length() > 0) {
177                        shapesString.append(";");
178                    }
179                    shapesString.append(s.encodeAsString(","));
180                }
181                if (shapesString.length() > 0) {
182                    shapes = shapesString.toString();
183                }
184            }
185            if (i.serverProjections != null && !i.serverProjections.isEmpty()) {
186                StringBuilder val = new StringBuilder();
187                for (String p : i.serverProjections) {
188                    if (val.length() > 0) {
189                        val.append(",");
190                    }
191                    val.append(p);
192                }
193                projections = val.toString();
194            }
195        }
196
197        @Override
198        public String toString() {
199            return "ImageryPreferenceEntry [name=" + name + "]";
200        }
201    }
202
203    /**
204     * Constructs a new {@code ImageryInfo}.
205     */
206    public ImageryInfo() {
207    }
208
209    public ImageryInfo(String name) {
210        this.name=name;
211    }
212
213    public ImageryInfo(String name, String url) {
214        this.name=name;
215        setExtendedUrl(url);
216    }
217
218    public ImageryInfo(String name, String url, String eulaAcceptanceRequired) {
219        this.name=name;
220        setExtendedUrl(url);
221        this.eulaAcceptanceRequired = eulaAcceptanceRequired;
222    }
223
224    public ImageryInfo(String name, String url, String eulaAcceptanceRequired, String cookies) {
225        this.name=name;
226        setExtendedUrl(url);
227        this.cookies=cookies;
228        this.eulaAcceptanceRequired = eulaAcceptanceRequired;
229    }
230
231    public ImageryInfo(String name, String url, String type, String eulaAcceptanceRequired, String cookies) {
232        this.name=name;
233        setExtendedUrl(url);
234        ImageryType t = ImageryType.fromUrlString(type);
235        this.cookies=cookies;
236        this.eulaAcceptanceRequired = eulaAcceptanceRequired;
237        if (t != null) {
238            this.imageryType = t;
239        }
240    }
241
242    public ImageryInfo(String name, String url, String cookies, double pixelPerDegree) {
243        this.name=name;
244        setExtendedUrl(url);
245        this.cookies=cookies;
246        this.pixelPerDegree=pixelPerDegree;
247    }
248
249    public ImageryInfo(ImageryPreferenceEntry e) {
250        CheckParameterUtil.ensureParameterNotNull(e.name, "name");
251        CheckParameterUtil.ensureParameterNotNull(e.url, "url");
252        name = e.name;
253        url = e.url;
254        cookies = e.cookies;
255        eulaAcceptanceRequired = e.eula;
256        imageryType = ImageryType.fromUrlString(e.type);
257        if (imageryType == null) throw new IllegalArgumentException("unknown type");
258        pixelPerDegree = e.pixel_per_eastnorth;
259        defaultMaxZoom = e.max_zoom;
260        defaultMinZoom = e.min_zoom;
261        if (e.bounds != null) {
262            bounds = new ImageryBounds(e.bounds, ",");
263            if (e.shapes != null) {
264                try {
265                    for (String s : e.shapes.split(";")) {
266                        bounds.addShape(new Shape(s, ","));
267                    }
268                } catch (IllegalArgumentException ex) {
269                    Main.warn(ex);
270                }
271            }
272        }
273        if (e.projections != null) {
274            serverProjections = Arrays.asList(e.projections.split(","));
275        }
276        attributionText = e.attribution_text;
277        attributionLinkURL = e.attribution_url;
278        attributionImage = e.logo_image;
279        attributionImageURL = e.logo_url;
280        termsOfUseText = e.terms_of_use_text;
281        termsOfUseURL = e.terms_of_use_url;
282        countryCode = e.country_code;
283        icon = e.icon;
284    }
285
286    public ImageryInfo(ImageryInfo i) {
287        this.name = i.name;
288        this.url = i.url;
289        this.defaultEntry = i.defaultEntry;
290        this.cookies = i.cookies;
291        this.eulaAcceptanceRequired = null;
292        this.imageryType = i.imageryType;
293        this.pixelPerDegree = i.pixelPerDegree;
294        this.defaultMaxZoom = i.defaultMaxZoom;
295        this.defaultMinZoom = i.defaultMinZoom;
296        this.bounds = i.bounds;
297        this.serverProjections = i.serverProjections;
298        this.attributionText = i.attributionText;
299        this.attributionLinkURL = i.attributionLinkURL;
300        this.attributionImage = i.attributionImage;
301        this.attributionImageURL = i.attributionImageURL;
302        this.termsOfUseText = i.termsOfUseText;
303        this.termsOfUseURL = i.termsOfUseURL;
304        this.countryCode = i.countryCode;
305        this.icon = i.icon;
306    }
307
308    @Override
309    public boolean equals(Object o) {
310        if (this == o) return true;
311        if (o == null || getClass() != o.getClass()) return false;
312
313        ImageryInfo that = (ImageryInfo) o;
314
315        if (imageryType != that.imageryType) return false;
316        if (url != null ? !url.equals(that.url) : that.url != null) return false;
317        if (name != null ? !name.equals(that.name) : that.name != null) return false;
318
319        return true;
320    }
321
322    @Override
323    public int hashCode() {
324        int result = url != null ? url.hashCode() : 0;
325        result = 31 * result + (imageryType != null ? imageryType.hashCode() : 0);
326        return result;
327    }
328
329    @Override
330    public String toString() {
331        return "ImageryInfo{" +
332                "name='" + name + '\'' +
333                ", countryCode='" + countryCode + '\'' +
334                ", url='" + url + '\'' +
335                ", imageryType=" + imageryType +
336                '}';
337    }
338
339    @Override
340    public int compareTo(ImageryInfo in)
341    {
342        int i = countryCode.compareTo(in.countryCode);
343        if (i == 0) {
344            i = name.compareTo(in.name);
345        }
346        if (i == 0) {
347            i = url.compareTo(in.url);
348        }
349        if (i == 0) {
350            i = Double.compare(pixelPerDegree, in.pixelPerDegree);
351        }
352        return i;
353    }
354
355    public boolean equalsBaseValues(ImageryInfo in)
356    {
357        return url.equals(in.url);
358    }
359
360    public void setPixelPerDegree(double ppd) {
361        this.pixelPerDegree = ppd;
362    }
363
364    public void setDefaultMaxZoom(int defaultMaxZoom) {
365        this.defaultMaxZoom = defaultMaxZoom;
366    }
367
368    public void setDefaultMinZoom(int defaultMinZoom) {
369        this.defaultMinZoom = defaultMinZoom;
370    }
371
372    public void setBounds(ImageryBounds b) {
373        this.bounds = b;
374    }
375
376    public ImageryBounds getBounds() {
377        return bounds;
378    }
379
380    @Override
381    public boolean requiresAttribution() {
382        return attributionText != null || attributionImage != null || termsOfUseText != null || termsOfUseURL != null;
383    }
384
385    @Override
386    public String getAttributionText(int zoom, Coordinate topLeft, Coordinate botRight) {
387        return attributionText;
388    }
389
390    @Override
391    public String getAttributionLinkURL() {
392        return attributionLinkURL;
393    }
394
395    @Override
396    public Image getAttributionImage() {
397        ImageIcon i = ImageProvider.getIfAvailable(attributionImage);
398        if (i != null) {
399            return i.getImage();
400        }
401        return null;
402    }
403
404    @Override
405    public String getAttributionImageURL() {
406        return attributionImageURL;
407    }
408
409    @Override
410    public String getTermsOfUseText() {
411        return termsOfUseText;
412    }
413
414    @Override
415    public String getTermsOfUseURL() {
416        return termsOfUseURL;
417    }
418
419    public void setAttributionText(String text) {
420        attributionText = text;
421    }
422
423    public void setAttributionImageURL(String text) {
424        attributionImageURL = text;
425    }
426
427    public void setAttributionImage(String text) {
428        attributionImage = text;
429    }
430
431    public void setAttributionLinkURL(String text) {
432        attributionLinkURL = text;
433    }
434
435    public void setTermsOfUseText(String text) {
436        termsOfUseText = text;
437    }
438
439    public void setTermsOfUseURL(String text) {
440        termsOfUseURL = text;
441    }
442
443    public void setExtendedUrl(String url) {
444        CheckParameterUtil.ensureParameterNotNull(url);
445
446        // Default imagery type is WMS
447        this.url = url;
448        this.imageryType = ImageryType.WMS;
449
450        defaultMaxZoom = 0;
451        defaultMinZoom = 0;
452        for (ImageryType type : ImageryType.values()) {
453            Matcher m = Pattern.compile(type.getUrlString()+"(?:\\[(?:(\\d+),)?(\\d+)\\])?:(.*)").matcher(url);
454            if (m.matches()) {
455                this.url = m.group(3);
456                this.imageryType = type;
457                if (m.group(2) != null) {
458                    defaultMaxZoom = Integer.valueOf(m.group(2));
459                }
460                if (m.group(1) != null) {
461                    defaultMinZoom = Integer.valueOf(m.group(1));
462                }
463                break;
464            }
465        }
466
467        if (serverProjections == null || serverProjections.isEmpty()) {
468            try {
469                serverProjections = new ArrayList<String>();
470                Matcher m = Pattern.compile(".*\\{PROJ\\(([^)}]+)\\)\\}.*").matcher(url.toUpperCase());
471                if(m.matches()) {
472                    for(String p : m.group(1).split(","))
473                        serverProjections.add(p);
474                }
475            } catch (Exception e) {
476                Main.warn(e);
477            }
478        }
479    }
480
481    public String getName() {
482        return this.name;
483    }
484
485    public void setName(String name) {
486        this.name = name;
487    }
488
489    public String getUrl() {
490        return this.url;
491    }
492
493    public void setUrl(String url) {
494        this.url = url;
495    }
496
497    public boolean isDefaultEntry() {
498        return defaultEntry;
499    }
500
501    public void setDefaultEntry(boolean defaultEntry) {
502        this.defaultEntry = defaultEntry;
503    }
504
505    public String getCookies() {
506        return this.cookies;
507    }
508
509    public double getPixelPerDegree() {
510        return this.pixelPerDegree;
511    }
512
513    public int getMaxZoom() {
514        return this.defaultMaxZoom;
515    }
516
517    public int getMinZoom() {
518        return this.defaultMinZoom;
519    }
520
521    public String getEulaAcceptanceRequired() {
522        return eulaAcceptanceRequired;
523    }
524
525    public void setEulaAcceptanceRequired(String eulaAcceptanceRequired) {
526        this.eulaAcceptanceRequired = eulaAcceptanceRequired;
527    }
528
529    public String getCountryCode() {
530        return countryCode;
531    }
532
533    public void setCountryCode(String countryCode) {
534        this.countryCode = countryCode;
535    }
536
537    public String getIcon() {
538        return icon;
539    }
540
541    public void setIcon(String icon) {
542        this.icon = icon;
543    }
544
545    /**
546     * Get the projections supported by the server. Only relevant for
547     * WMS-type ImageryInfo at the moment.
548     * @return null, if no projections have been specified; the list
549     * of supported projections otherwise.
550     */
551    public List<String> getServerProjections() {
552        if (serverProjections == null)
553            return Collections.emptyList();
554        return Collections.unmodifiableList(serverProjections);
555    }
556
557    public void setServerProjections(Collection<String> serverProjections) {
558        this.serverProjections = new ArrayList<String>(serverProjections);
559    }
560
561    public String getExtendedUrl() {
562        return imageryType.getUrlString() + (defaultMaxZoom != 0
563            ? "["+(defaultMinZoom != 0 ? defaultMinZoom+",":"")+defaultMaxZoom+"]" : "") + ":" + url;
564    }
565
566    public String getToolbarName()
567    {
568        String res = name;
569        if(pixelPerDegree != 0.0) {
570            res += "#PPD="+pixelPerDegree;
571        }
572        return res;
573    }
574
575    public String getMenuName()
576    {
577        String res = name;
578        if(pixelPerDegree != 0.0) {
579            res += " ("+pixelPerDegree+")";
580        }
581        return res;
582    }
583
584    public boolean hasAttribution()
585    {
586        return attributionText != null;
587    }
588
589    public void copyAttribution(ImageryInfo i)
590    {
591        this.attributionImage = i.attributionImage;
592        this.attributionImageURL = i.attributionImageURL;
593        this.attributionText = i.attributionText;
594        this.attributionLinkURL = i.attributionLinkURL;
595        this.termsOfUseText = i.termsOfUseText;
596        this.termsOfUseURL = i.termsOfUseURL;
597    }
598
599    /**
600     * Applies the attribution from this object to a TMSTileSource.
601     */
602    public void setAttribution(AbstractTileSource s) {
603        if (attributionText != null) {
604            if (attributionText.equals("osm")) {
605                s.setAttributionText(new Mapnik().getAttributionText(0, null, null));
606            } else {
607                s.setAttributionText(attributionText);
608            }
609        }
610        if (attributionLinkURL != null) {
611            if (attributionLinkURL.equals("osm")) {
612                s.setAttributionLinkURL(new Mapnik().getAttributionLinkURL());
613            } else {
614                s.setAttributionLinkURL(attributionLinkURL);
615            }
616        }
617        if (attributionImage != null) {
618            ImageIcon i = ImageProvider.getIfAvailable(null, attributionImage);
619            if (i != null) {
620                s.setAttributionImage(i.getImage());
621            }
622        }
623        if (attributionImageURL != null) {
624            s.setAttributionImageURL(attributionImageURL);
625        }
626        if (termsOfUseText != null) {
627            s.setTermsOfUseText(termsOfUseText);
628        }
629        if (termsOfUseURL != null) {
630            if (termsOfUseURL.equals("osm")) {
631                s.setTermsOfUseURL(new Mapnik().getTermsOfUseURL());
632            } else {
633                s.setTermsOfUseURL(termsOfUseURL);
634            }
635        }
636    }
637
638    public ImageryType getImageryType() {
639        return imageryType;
640    }
641
642    public void setImageryType(ImageryType imageryType) {
643        this.imageryType = imageryType;
644    }
645
646    /**
647     * Returns true if this layer's URL is matched by one of the regular
648     * expressions kept by the current OsmApi instance.
649     */
650    public boolean isBlacklisted() {
651        return OsmApi.getOsmApi().getCapabilities().isOnImageryBlacklist(this.url);
652    }
653}