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.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;
015import java.util.regex.Pattern;
016
017import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
018import org.openstreetmap.josm.Main;
019import org.openstreetmap.josm.data.coor.EastNorth;
020import org.openstreetmap.josm.data.projection.Projection;
021import org.openstreetmap.josm.gui.layer.WMSLayer;
022import org.openstreetmap.josm.tools.CheckParameterUtil;
023
024/**
025 * Tile Source handling WMS providers
026 *
027 * @author Wiktor Niesiobędzki
028 * @since 8526
029 */
030public class TemplatedWMSTileSource extends AbstractWMSTileSource implements TemplatedTileSource {
031    private final Map<String, String> headers = new ConcurrentHashMap<>();
032    private final Set<String> serverProjections;
033    // CHECKSTYLE.OFF: SingleSpaceSeparator
034    private static final Pattern PATTERN_HEADER = Pattern.compile("\\{header\\(([^,]+),([^}]+)\\)\\}");
035    private static final Pattern PATTERN_PROJ   = Pattern.compile("\\{proj\\}");
036    private static final Pattern PATTERN_WKID   = Pattern.compile("\\{wkid\\}");
037    private static final Pattern PATTERN_BBOX   = Pattern.compile("\\{bbox\\}");
038    private static final Pattern PATTERN_W      = Pattern.compile("\\{w\\}");
039    private static final Pattern PATTERN_S      = Pattern.compile("\\{s\\}");
040    private static final Pattern PATTERN_E      = Pattern.compile("\\{e\\}");
041    private static final Pattern PATTERN_N      = Pattern.compile("\\{n\\}");
042    private static final Pattern PATTERN_WIDTH  = Pattern.compile("\\{width\\}");
043    private static final Pattern PATTERN_HEIGHT = Pattern.compile("\\{height\\}");
044    private static final Pattern PATTERN_PARAM  = Pattern.compile("\\{([^}]+)\\}");
045    // CHECKSTYLE.ON: SingleSpaceSeparator
046
047    private static final NumberFormat LATLON_FORMAT = new DecimalFormat("###0.0000000", new DecimalFormatSymbols(Locale.US));
048
049    private static final Pattern[] ALL_PATTERNS = {
050        PATTERN_HEADER, PATTERN_PROJ, PATTERN_WKID, PATTERN_BBOX, PATTERN_W, PATTERN_S, PATTERN_E, PATTERN_N, PATTERN_WIDTH, PATTERN_HEIGHT
051    };
052
053    /**
054     * Creates a tile source based on imagery info
055     * @param info imagery info
056     * @param tileProjection the tile projection
057     */
058    public TemplatedWMSTileSource(ImageryInfo info, Projection tileProjection) {
059        super(info, tileProjection);
060        this.serverProjections = new TreeSet<>(info.getServerProjections());
061        this.headers.putAll(info.getCustomHttpHeaders());
062        handleTemplate();
063        initProjection();
064    }
065
066    @Override
067    public int getDefaultTileSize() {
068        return WMSLayer.PROP_IMAGE_SIZE.get();
069    }
070
071    @Override
072    public String getTileUrl(int zoom, int tilex, int tiley) {
073        String myProjCode = getServerCRS();
074
075        EastNorth nw = getTileEastNorth(tilex, tiley, zoom);
076        EastNorth se = getTileEastNorth(tilex + 1, tiley + 1, zoom);
077
078        double w = nw.getX();
079        double n = nw.getY();
080
081        double s = se.getY();
082        double e = se.getX();
083
084        if ("EPSG:4326".equals(myProjCode) && !serverProjections.contains(myProjCode) && serverProjections.contains("CRS:84")) {
085            myProjCode = "CRS:84";
086        }
087
088        // Bounding box coordinates have to be switched for WMS 1.3.0 EPSG:4326.
089        //
090        // Background:
091        //
092        // bbox=x_min,y_min,x_max,y_max
093        //
094        //      SRS=... is WMS 1.1.1
095        //      CRS=... is WMS 1.3.0
096        //
097        // The difference:
098        //      For SRS x is east-west and y is north-south
099        //      For CRS x and y are as specified by the EPSG
100        //          E.g. [1] lists lat as first coordinate axis and lot as second, so it is switched for EPSG:4326.
101        //          For most other EPSG code there seems to be no difference.
102        // CHECKSTYLE.OFF: LineLength
103        // [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
104        // CHECKSTYLE.ON: LineLength
105        boolean switchLatLon = false;
106        if (baseUrl.toLowerCase(Locale.US).contains("crs=epsg:4326")) {
107            switchLatLon = true;
108        } else if (baseUrl.toLowerCase(Locale.US).contains("crs=")) {
109            // assume WMS 1.3.0
110            switchLatLon = Main.getProjection().switchXY();
111        }
112        String bbox = getBbox(zoom, tilex, tiley, switchLatLon);
113
114        // Using StringBuffer and generic PATTERN_PARAM matcher gives 2x performance improvement over replaceAll
115        StringBuffer url = new StringBuffer(baseUrl.length());
116        Matcher matcher = PATTERN_PARAM.matcher(baseUrl);
117        while (matcher.find()) {
118            String replacement;
119            switch (matcher.group(1)) {
120            case "proj":
121                replacement = myProjCode;
122                break;
123            case "wkid":
124                replacement = myProjCode.startsWith("EPSG:") ? myProjCode.substring(5) : myProjCode;
125                break;
126            case "bbox":
127                replacement = bbox;
128                break;
129            case "w":
130                replacement = LATLON_FORMAT.format(w);
131                break;
132            case "s":
133                replacement = LATLON_FORMAT.format(s);
134                break;
135            case "e":
136                replacement = LATLON_FORMAT.format(e);
137                break;
138            case "n":
139                replacement = LATLON_FORMAT.format(n);
140                break;
141            case "width":
142            case "height":
143                replacement = String.valueOf(getTileSize());
144                break;
145            default:
146                replacement = '{' + matcher.group(1) + '}';
147            }
148            matcher.appendReplacement(url, replacement);
149        }
150        matcher.appendTail(url);
151        return url.toString().replace(" ", "%20");
152    }
153
154    @Override
155    public String getTileId(int zoom, int tilex, int tiley) {
156        return getTileUrl(zoom, tilex, tiley);
157    }
158
159    @Override
160    public Map<String, String> getHeaders() {
161        return headers;
162    }
163
164    /**
165     * Checks if url is acceptable by this Tile Source
166     * @param url URL to check
167     */
168    public static void checkUrl(String url) {
169        CheckParameterUtil.ensureParameterNotNull(url, "url");
170        Matcher m = PATTERN_PARAM.matcher(url);
171        while (m.find()) {
172            boolean isSupportedPattern = false;
173            for (Pattern pattern : ALL_PATTERNS) {
174                if (pattern.matcher(m.group()).matches()) {
175                    isSupportedPattern = true;
176                    break;
177                }
178            }
179            if (!isSupportedPattern) {
180                throw new IllegalArgumentException(
181                        tr("{0} is not a valid WMS argument. Please check this server URL:\n{1}", m.group(), url));
182            }
183        }
184    }
185
186    private void handleTemplate() {
187        // Capturing group pattern on switch values
188        StringBuffer output = new StringBuffer();
189        Matcher matcher = PATTERN_HEADER.matcher(this.baseUrl);
190        while (matcher.find()) {
191            headers.put(matcher.group(1), matcher.group(2));
192            matcher.appendReplacement(output, "");
193        }
194        matcher.appendTail(output);
195        this.baseUrl = output.toString();
196    }
197}