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 */
031public class TemplatedTMSTileSource extends TMSTileSource implements TemplatedTileSource {
032
033    private Random rand;
034    private String[] randomParts;
035    private final Map<String, String> headers = new HashMap<>();
036
037    // CHECKSTYLE.OFF: SingleSpaceSeparator
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    // CHECKSTYLE.ON: SingleSpaceSeparator
047
048    private static final String[] ALL_PATTERNS = {
049        PATTERN_HEADER, PATTERN_ZOOM, PATTERN_X, PATTERN_Y, PATTERN_Y_YAHOO, PATTERN_NEG_Y, 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        String cookies = info.getCookies();
059        if (cookies != null && !cookies.isEmpty()) {
060            headers.put(COOKIE_HEADER, cookies);
061        }
062        handleTemplate();
063    }
064
065    private void handleTemplate() {
066        // Capturing group pattern on switch values
067        Matcher m = Pattern.compile(".*"+PATTERN_SWITCH+".*").matcher(baseUrl);
068        if (m.matches()) {
069            rand = new Random();
070            randomParts = m.group(1).split(",");
071        }
072        Pattern pattern = Pattern.compile(PATTERN_HEADER);
073        StringBuffer output = new StringBuffer();
074        Matcher matcher = pattern.matcher(baseUrl);
075        while (matcher.find()) {
076            headers.put(matcher.group(1), matcher.group(2));
077            matcher.appendReplacement(output, "");
078        }
079        matcher.appendTail(output);
080        baseUrl = output.toString();
081    }
082
083    @Override
084    public Map<String, String> getHeaders() {
085        return headers;
086    }
087
088    @Override
089    public String getTileUrl(int zoom, int tilex, int tiley) {
090        int finalZoom = zoom;
091        Matcher m = Pattern.compile(".*"+PATTERN_ZOOM+".*").matcher(this.baseUrl);
092        if (m.matches()) {
093            if (m.group(1) != null) {
094                finalZoom = Integer.parseInt(m.group(1))-zoom;
095            }
096            if (m.group(2) != null) {
097                String ofs = m.group(2);
098                if (ofs.startsWith("+"))
099                    ofs = ofs.substring(1);
100                finalZoom += Integer.parseInt(ofs);
101            }
102        }
103        String r = this.baseUrl
104            .replaceAll(PATTERN_ZOOM, Integer.toString(finalZoom))
105            .replaceAll(PATTERN_X, Integer.toString(tilex))
106            .replaceAll(PATTERN_Y, Integer.toString(tiley))
107            .replaceAll(PATTERN_Y_YAHOO, Integer.toString((int) Math.pow(2, zoom-1)-1-tiley))
108            .replaceAll(PATTERN_NEG_Y, Integer.toString((int) Math.pow(2, zoom)-1-tiley));
109        if (rand != null) {
110            r = r.replaceAll(PATTERN_SWITCH, randomParts[rand.nextInt(randomParts.length)]);
111        }
112        return r;
113    }
114
115    /**
116     * Checks if url is acceptable by this Tile Source
117     * @param url URL to check
118     */
119    public static void checkUrl(String url) {
120        assert url != null && !"".equals(url) : "URL cannot be null or empty";
121        Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);
122        while (m.find()) {
123            boolean isSupportedPattern = false;
124            for (String pattern : ALL_PATTERNS) {
125                if (m.group().matches(pattern)) {
126                    isSupportedPattern = true;
127                    break;
128                }
129            }
130            if (!isSupportedPattern) {
131                throw new IllegalArgumentException(
132                        m.group() + " is not a valid TMS argument. Please check this server URL:\n" + url);
133            }
134        }
135    }
136}