001// License: GPL. For details, see Readme.txt file.
002package org.openstreetmap.gui.jmapviewer.tilesources;
003
004import java.util.HashMap;
005import java.util.Map;
006import java.util.Random;
007import java.util.regex.Matcher;
008import java.util.regex.Pattern;
009
010import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
011
012/**
013 * Handles templated TMS Tile Source. Templated means, that some patterns within
014 * URL gets substituted.
015 *
016 * Supported parameters
017 * {zoom} - substituted with zoom level
018 * {z} - as above
019 * {NUMBER-zoom} - substituted with result of equation "NUMBER - zoom",
020 *                  eg. {20-zoom} for zoom level 15 will result in 5 in this place
021 * {zoom+number} - substituted with result of equation "zoom + number",
022 *                 eg. {zoom+5} for zoom level 15 will result in 20.
023 * {x} - substituted with X tile number
024 * {y} - substituted with Y tile number
025 * {!y} - substituted with Yahoo Y tile number
026 * {-y} - substituted with reversed Y tile number
027 * {switch:VAL_A,VAL_B,VAL_C,...} - substituted with one of VAL_A, VAL_B, VAL_C. Usually
028 *                                  used to specify many tile servers
029 * {header:(HEADER_NAME,HEADER_VALUE)} - sets the headers to be sent to tile server
030 */
031
032public class TemplatedTMSTileSource extends TMSTileSource implements TemplatedTileSource {
033
034    private Random rand;
035    private String[] randomParts;
036    private final Map<String, String> headers = new HashMap<>();
037
038    private static final String COOKIE_HEADER   = "Cookie";
039    private static final String PATTERN_ZOOM    = "\\{(?:(\\d+)-)?z(?:oom)?([+-]\\d+)?\\}";
040    private static final String PATTERN_X       = "\\{x\\}";
041    private static final String PATTERN_Y       = "\\{y\\}";
042    private static final String PATTERN_Y_YAHOO = "\\{!y\\}";
043    private static final String PATTERN_NEG_Y   = "\\{-y\\}";
044    private static final String PATTERN_SWITCH  = "\\{switch:([^}]+)\\}";
045    private static final String PATTERN_HEADER  = "\\{header\\(([^,]+),([^}]+)\\)\\}";
046
047    private static final String[] ALL_PATTERNS = {
048        PATTERN_HEADER, PATTERN_ZOOM, PATTERN_X, PATTERN_Y, PATTERN_Y_YAHOO, PATTERN_NEG_Y,
049        PATTERN_SWITCH
050    };
051
052    /**
053     * Creates Templated TMS Tile Source based on ImageryInfo
054     * @param info imagery info
055     */
056    public TemplatedTMSTileSource(TileSourceInfo info) {
057        super(info);
058        if (info.getCookies() != null) {
059            headers.put(COOKIE_HEADER, info.getCookies());
060        }
061        handleTemplate();
062    }
063
064    private void handleTemplate() {
065        // Capturing group pattern on switch values
066        Matcher m = Pattern.compile(".*"+PATTERN_SWITCH+".*").matcher(baseUrl);
067        if (m.matches()) {
068            rand = new Random();
069            randomParts = m.group(1).split(",");
070        }
071        Pattern pattern = Pattern.compile(PATTERN_HEADER);
072        StringBuffer output = new StringBuffer();
073        Matcher matcher = pattern.matcher(baseUrl);
074        while (matcher.find()) {
075            headers.put(matcher.group(1), matcher.group(2));
076            matcher.appendReplacement(output, "");
077        }
078        matcher.appendTail(output);
079        baseUrl = output.toString();
080    }
081
082    @Override
083    public Map<String, String> getHeaders() {
084        return headers;
085    }
086
087    @Override
088    public String getTileUrl(int zoom, int tilex, int tiley) {
089        int finalZoom = zoom;
090        Matcher m = Pattern.compile(".*"+PATTERN_ZOOM+".*").matcher(this.baseUrl);
091        if (m.matches()) {
092            if (m.group(1) != null) {
093                finalZoom = Integer.parseInt(m.group(1))-zoom;
094            }
095            if (m.group(2) != null) {
096                String ofs = m.group(2);
097                if (ofs.startsWith("+"))
098                    ofs = ofs.substring(1);
099                finalZoom += Integer.parseInt(ofs);
100            }
101        }
102        String r = this.baseUrl
103            .replaceAll(PATTERN_ZOOM, Integer.toString(finalZoom))
104            .replaceAll(PATTERN_X, Integer.toString(tilex))
105            .replaceAll(PATTERN_Y, Integer.toString(tiley))
106            .replaceAll(PATTERN_Y_YAHOO, Integer.toString((int) Math.pow(2, zoom-1)-1-tiley))
107            .replaceAll(PATTERN_NEG_Y, Integer.toString((int) Math.pow(2, zoom)-1-tiley));
108        if (rand != null) {
109            r = r.replaceAll(PATTERN_SWITCH, randomParts[rand.nextInt(randomParts.length)]);
110        }
111        return r;
112    }
113
114    /**
115     * Checks if url is acceptable by this Tile Source
116     * @param url URL to check
117     */
118    public static void checkUrl(String url) {
119        assert url != null && !"".equals(url) : "URL cannot be null or empty";
120        Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);
121        while (m.find()) {
122            boolean isSupportedPattern = false;
123            for (String pattern : ALL_PATTERNS) {
124                if (m.group().matches(pattern)) {
125                    isSupportedPattern = true;
126                    break;
127                }
128            }
129            if (!isSupportedPattern) {
130                throw new IllegalArgumentException(
131                        m.group() + " is not a valid TMS argument. Please check this server URL:\n" + url);
132            }
133        }
134    }
135}