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.data.coor.EastNorth;
019import org.openstreetmap.josm.data.projection.Projection;
020import org.openstreetmap.josm.data.projection.ProjectionRegistry;
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    private final boolean switchLatLon;
054    /**
055     * Creates a tile source based on imagery info
056     * @param info imagery info
057     * @param tileProjection the tile projection
058     */
059    public TemplatedWMSTileSource(ImageryInfo info, Projection tileProjection) {
060        super(info, tileProjection);
061        this.serverProjections = new TreeSet<>(info.getServerProjections());
062        this.headers.putAll(info.getCustomHttpHeaders());
063        handleTemplate();
064        initProjection();
065        // Bounding box coordinates have to be switched for WMS 1.3.0 EPSG:4326.
066        //
067        // Background:
068        //
069        // bbox=x_min,y_min,x_max,y_max
070        //
071        //      SRS=... is WMS 1.1.1
072        //      CRS=... is WMS 1.3.0
073        //
074        // The difference:
075        //      For SRS x is east-west and y is north-south
076        //      For CRS x and y are as specified by the EPSG
077        //          E.g. [1] lists lat as first coordinate axis and lot as second, so it is switched for EPSG:4326.
078        //          For most other EPSG code there seems to be no difference.
079        // CHECKSTYLE.OFF: LineLength
080        // [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
081        // CHECKSTYLE.ON: LineLength
082        if (baseUrl.toLowerCase(Locale.US).contains("crs=epsg:4326")) {
083            switchLatLon = true;
084        } else if (baseUrl.toLowerCase(Locale.US).contains("crs=")) {
085            // assume WMS 1.3.0
086            switchLatLon = ProjectionRegistry.getProjection().switchXY();
087        } else {
088            switchLatLon = false;
089        }
090    }
091
092    @Override
093    public int getDefaultTileSize() {
094        return WMSLayer.PROP_IMAGE_SIZE.get();
095    }
096
097    @Override
098    public String getTileUrl(int zoom, int tilex, int tiley) {
099        String myProjCode = getServerCRS();
100
101        EastNorth nw = getTileEastNorth(tilex, tiley, zoom);
102        EastNorth se = getTileEastNorth(tilex + 1, tiley + 1, zoom);
103
104        double w = nw.getX();
105        double n = nw.getY();
106
107        double s = se.getY();
108        double e = se.getX();
109
110        if ("EPSG:4326".equals(myProjCode) && !serverProjections.contains(myProjCode) && serverProjections.contains("CRS:84")) {
111            myProjCode = "CRS:84";
112        }
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 = getBbox(zoom, tilex, tiley, switchLatLon);
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}