001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.imagery;
003
004import static org.openstreetmap.josm.data.imagery.ImageryPatterns.PATTERN_PARAM;
005
006import java.text.DecimalFormat;
007import java.text.DecimalFormatSymbols;
008import java.text.NumberFormat;
009import java.util.Locale;
010import java.util.Map;
011import java.util.Set;
012import java.util.TreeSet;
013import java.util.concurrent.ConcurrentHashMap;
014import java.util.regex.Matcher;
015
016import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
017import org.openstreetmap.josm.data.coor.EastNorth;
018import org.openstreetmap.josm.data.projection.Projection;
019import org.openstreetmap.josm.gui.layer.WMSLayer;
020import org.openstreetmap.josm.tools.Utils;
021
022/**
023 * Tile Source handling WMS providers
024 *
025 * @author Wiktor Niesiobędzki
026 * @since 8526
027 */
028public class TemplatedWMSTileSource extends AbstractWMSTileSource implements TemplatedTileSource {
029
030    private static final NumberFormat LATLON_FORMAT = new DecimalFormat("###0.0000000", new DecimalFormatSymbols(Locale.US));
031
032    private final Set<String> serverProjections;
033    private final Map<String, String> headers = new ConcurrentHashMap<>();
034    private final String date;
035    private final boolean belowWMS130;
036
037    /**
038     * Creates a tile source based on imagery info
039     * @param info imagery info
040     * @param tileProjection the tile projection
041     */
042    public TemplatedWMSTileSource(ImageryInfo info, Projection tileProjection) {
043        super(info, tileProjection);
044        this.serverProjections = new TreeSet<>(info.getServerProjections());
045        this.headers.putAll(info.getCustomHttpHeaders());
046        this.date = info.getDate();
047        this.baseUrl = ImageryPatterns.handleHeaderTemplate(baseUrl, headers);
048        initProjection();
049        // Bounding box coordinates have to be switched for WMS 1.3.0 EPSG:4326.
050        //
051        // Background:
052        //
053        // bbox=x_min,y_min,x_max,y_max
054        //
055        //      SRS=... is WMS 1.1.1
056        //      CRS=... is WMS 1.3.0
057        //
058        // The difference:
059        //      For SRS x is east-west and y is north-south
060        //      For CRS x and y are as specified by the EPSG
061        //          E.g. [1] lists lat as first coordinate axis and lot as second, so it is switched for EPSG:4326.
062        //          For most other EPSG code there seems to be no difference.
063        // CHECKSTYLE.OFF: LineLength
064        // [1] https://www.epsg-registry.org/report.htm?type=selection&entity=urn:ogc:def:crs:EPSG::4326&reportDetail=short&style=urn:uuid:report-style:default-with-code&style_name=OGP%20Default%20With%20Code&title=EPSG:4326
065        // CHECKSTYLE.ON: LineLength
066        belowWMS130 = !baseUrl.toLowerCase(Locale.US).contains("crs=");
067    }
068
069    @Override
070    public int getDefaultTileSize() {
071        return WMSLayer.PROP_IMAGE_SIZE.get();
072    }
073
074    @Override
075    public String getTileUrl(int zoom, int tilex, int tiley) {
076        String myProjCode = getServerCRS();
077
078        EastNorth nw = getTileEastNorth(tilex, tiley, zoom);
079        EastNorth se = getTileEastNorth(tilex + 1, tiley + 1, zoom);
080
081        double w = nw.getX();
082        double n = nw.getY();
083
084        double s = se.getY();
085        double e = se.getX();
086
087        if ("EPSG:4326".equals(myProjCode) && !serverProjections.contains(myProjCode) && serverProjections.contains("CRS:84")) {
088            myProjCode = "CRS:84";
089        }
090
091        // Using StringBuffer and generic PATTERN_PARAM matcher gives 2x performance improvement over replaceAll
092        StringBuffer url = new StringBuffer(baseUrl.length());
093        Matcher matcher = PATTERN_PARAM.matcher(baseUrl);
094        while (matcher.find()) {
095            String replacement;
096            switch (matcher.group(1)) {
097            case "proj":
098                replacement = myProjCode;
099                break;
100            case "wkid":
101                replacement = myProjCode.startsWith("EPSG:") ? myProjCode.substring(5) : myProjCode;
102                break;
103            case "bbox":
104                replacement = getBbox(zoom, tilex, tiley, !belowWMS130 && getTileProjection().switchXY());
105                break;
106            case "w":
107                replacement = LATLON_FORMAT.format(w);
108                break;
109            case "s":
110                replacement = LATLON_FORMAT.format(s);
111                break;
112            case "e":
113                replacement = LATLON_FORMAT.format(e);
114                break;
115            case "n":
116                replacement = LATLON_FORMAT.format(n);
117                break;
118            case "width":
119            case "height":
120                replacement = String.valueOf(getTileSize());
121                break;
122            case "time":
123                replacement = Utils.encodeUrl(date);
124                break;
125            default:
126                replacement = '{' + matcher.group(1) + '}';
127            }
128            matcher.appendReplacement(url, replacement);
129        }
130        matcher.appendTail(url);
131        return url.toString().replace(" ", "%20");
132    }
133
134    @Override
135    public String getTileId(int zoom, int tilex, int tiley) {
136        return getTileUrl(zoom, tilex, tiley);
137    }
138
139    @Override
140    public Map<String, String> getHeaders() {
141        return headers;
142    }
143
144    /**
145     * Checks if url is acceptable by this Tile Source
146     * @param url URL to check
147     */
148    public static void checkUrl(String url) {
149        ImageryPatterns.checkWmsUrlPatterns(url);
150    }
151}