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