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        handleTemplate();
062        initProjection();
063    }
064
065    @Override
066    public int getDefaultTileSize() {
067        return WMSLayer.PROP_IMAGE_SIZE.get();
068    }
069
070    @Override
071    public String getTileUrl(int zoom, int tilex, int tiley) {
072        String myProjCode = getServerCRS();
073
074        EastNorth nw = getTileEastNorth(tilex, tiley, zoom);
075        EastNorth se = getTileEastNorth(tilex + 1, tiley + 1, zoom);
076
077        double w = nw.getX();
078        double n = nw.getY();
079
080        double s = se.getY();
081        double e = se.getX();
082
083        if ("EPSG:4326".equals(myProjCode) && !serverProjections.contains(myProjCode) && serverProjections.contains("CRS:84")) {
084            myProjCode = "CRS:84";
085        }
086
087        // Bounding box coordinates have to be switched for WMS 1.3.0 EPSG:4326.
088        //
089        // Background:
090        //
091        // bbox=x_min,y_min,x_max,y_max
092        //
093        //      SRS=... is WMS 1.1.1
094        //      CRS=... is WMS 1.3.0
095        //
096        // The difference:
097        //      For SRS x is east-west and y is north-south
098        //      For CRS x and y are as specified by the EPSG
099        //          E.g. [1] lists lat as first coordinate axis and lot as second, so it is switched for EPSG:4326.
100        //          For most other EPSG code there seems to be no difference.
101        // CHECKSTYLE.OFF: LineLength
102        // [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
103        // CHECKSTYLE.ON: LineLength
104        boolean switchLatLon = false;
105        if (baseUrl.toLowerCase(Locale.US).contains("crs=epsg:4326")) {
106            switchLatLon = true;
107        } else if (baseUrl.toLowerCase(Locale.US).contains("crs=")) {
108            // assume WMS 1.3.0
109            switchLatLon = Main.getProjection().switchXY();
110        }
111        String bbox;
112        if (switchLatLon) {
113            bbox = String.format("%s,%s,%s,%s",
114                    LATLON_FORMAT.format(s), LATLON_FORMAT.format(w), LATLON_FORMAT.format(n), LATLON_FORMAT.format(e));
115        } else {
116            bbox = String.format("%s,%s,%s,%s",
117                    LATLON_FORMAT.format(w), LATLON_FORMAT.format(s), LATLON_FORMAT.format(e), LATLON_FORMAT.format(n));
118        }
119
120        // Using StringBuffer and generic PATTERN_PARAM matcher gives 2x performance improvement over replaceAll
121        StringBuffer url = new StringBuffer(baseUrl.length());
122        Matcher matcher = PATTERN_PARAM.matcher(baseUrl);
123        while (matcher.find()) {
124            String replacement;
125            switch (matcher.group(1)) {
126            case "proj":
127                replacement = myProjCode;
128                break;
129            case "wkid":
130                replacement = myProjCode.startsWith("EPSG:") ? myProjCode.substring(5) : myProjCode;
131                break;
132            case "bbox":
133                replacement = bbox;
134                break;
135            case "w":
136                replacement = LATLON_FORMAT.format(w);
137                break;
138            case "s":
139                replacement = LATLON_FORMAT.format(s);
140                break;
141            case "e":
142                replacement = LATLON_FORMAT.format(e);
143                break;
144            case "n":
145                replacement = LATLON_FORMAT.format(n);
146                break;
147            case "width":
148            case "height":
149                replacement = String.valueOf(getTileSize());
150                break;
151            default:
152                replacement = '{' + matcher.group(1) + '}';
153            }
154            matcher.appendReplacement(url, replacement);
155        }
156        matcher.appendTail(url);
157        return url.toString().replace(" ", "%20");
158    }
159
160    @Override
161    public String getTileId(int zoom, int tilex, int tiley) {
162        return getTileUrl(zoom, tilex, tiley);
163    }
164
165    @Override
166    public Map<String, String> getHeaders() {
167        return headers;
168    }
169
170    /**
171     * Checks if url is acceptable by this Tile Source
172     * @param url URL to check
173     */
174    public static void checkUrl(String url) {
175        CheckParameterUtil.ensureParameterNotNull(url, "url");
176        Matcher m = PATTERN_PARAM.matcher(url);
177        while (m.find()) {
178            boolean isSupportedPattern = false;
179            for (Pattern pattern : ALL_PATTERNS) {
180                if (pattern.matcher(m.group()).matches()) {
181                    isSupportedPattern = true;
182                    break;
183                }
184            }
185            if (!isSupportedPattern) {
186                throw new IllegalArgumentException(
187                        tr("{0} is not a valid WMS argument. Please check this server URL:\n{1}", m.group(), url));
188            }
189        }
190    }
191
192    private void handleTemplate() {
193        // Capturing group pattern on switch values
194        StringBuffer output = new StringBuffer();
195        Matcher matcher = PATTERN_HEADER.matcher(this.baseUrl);
196        while (matcher.find()) {
197            headers.put(matcher.group(1), matcher.group(2));
198            matcher.appendReplacement(output, "");
199        }
200        matcher.appendTail(output);
201        this.baseUrl = output.toString();
202    }
203}