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    private boolean inverse_zoom = false;
037    private int zoom_offset = 0;
038
039    // CHECKSTYLE.OFF: SingleSpaceSeparator
040    private static final String COOKIE_HEADER   = "Cookie";
041    private static final Pattern PATTERN_ZOOM    = Pattern.compile("\\{(?:(\\d+)-)?z(?:oom)?([+-]\\d+)?\\}");
042    private static final Pattern PATTERN_X       = Pattern.compile("\\{x\\}");
043    private static final Pattern PATTERN_Y       = Pattern.compile("\\{y\\}");
044    private static final Pattern PATTERN_Y_YAHOO = Pattern.compile("\\{!y\\}");
045    private static final Pattern PATTERN_NEG_Y   = Pattern.compile("\\{-y\\}");
046    private static final Pattern PATTERN_SWITCH  = Pattern.compile("\\{switch:([^}]+)\\}");
047    private static final Pattern PATTERN_HEADER  = Pattern.compile("\\{header\\(([^,]+),([^}]+)\\)\\}");
048    private static final Pattern PATTERN_PARAM  = Pattern.compile("\\{((?:\\d+-)?z(?:oom)?(:?[+-]\\d+)?|x|y|!y|-y|switch:([^}]+))\\}");
049    // CHECKSTYLE.ON: SingleSpaceSeparator
050
051    private static final Pattern[] ALL_PATTERNS = {
052        PATTERN_HEADER, PATTERN_ZOOM, PATTERN_X, PATTERN_Y, PATTERN_Y_YAHOO, PATTERN_NEG_Y, PATTERN_SWITCH
053    };
054
055    /**
056     * Creates Templated TMS Tile Source based on ImageryInfo
057     * @param info imagery info
058     */
059    public TemplatedTMSTileSource(TileSourceInfo info) {
060        super(info);
061        String cookies = info.getCookies();
062        if (cookies != null && !cookies.isEmpty()) {
063            headers.put(COOKIE_HEADER, cookies);
064        }
065        handleTemplate();
066    }
067
068    private void handleTemplate() {
069        // Capturing group pattern on switch values
070        Matcher m = PATTERN_SWITCH.matcher(baseUrl);
071        if (m.find()) {
072            rand = new Random();
073            randomParts = m.group(1).split(",");
074        }
075        StringBuffer output = new StringBuffer();
076        Matcher matcher = PATTERN_HEADER.matcher(baseUrl);
077        while (matcher.find()) {
078            headers.put(matcher.group(1), matcher.group(2));
079            matcher.appendReplacement(output, "");
080        }
081        matcher.appendTail(output);
082        baseUrl = output.toString();
083        m = PATTERN_ZOOM.matcher(this.baseUrl);
084        if (m.find()) {
085            if (m.group(1) != null) {
086                inverse_zoom = true;
087                zoom_offset = Integer.parseInt(m.group(1));
088            }
089            if (m.group(2) != null) {
090                String ofs = m.group(2);
091                if (ofs.startsWith("+"))
092                    ofs = ofs.substring(1);
093                zoom_offset += Integer.parseInt(ofs);
094            }
095        }
096
097    }
098
099    @Override
100    public Map<String, String> getHeaders() {
101        return headers;
102    }
103
104    @Override
105    public String getTileUrl(int zoom, int tilex, int tiley) {
106        StringBuffer url = new StringBuffer(baseUrl.length());
107        Matcher matcher = PATTERN_PARAM.matcher(baseUrl);
108        while (matcher.find()) {
109            String replacement = "replace";
110            switch (matcher.group(1)) {
111            case "z": // PATTERN_ZOOM
112            case "zoom":
113                replacement = Integer.toString((inverse_zoom ? -1 * zoom : zoom) + zoom_offset);
114                break;
115            case "x": // PATTERN_X
116                replacement = Integer.toString(tilex);
117                break;
118            case "y": // PATTERN_Y
119                replacement = Integer.toString(tiley);
120                break;
121            case "!y": // PATTERN_Y_YAHOO
122                replacement = Integer.toString((int) Math.pow(2, zoom-1)-1-tiley);
123                break;
124            case "-y": // PATTERN_NEG_Y
125                replacement = Integer.toString((int) Math.pow(2, zoom)-1-tiley);
126                break;
127            case "switch:":
128                replacement = randomParts[rand.nextInt(randomParts.length)];
129                break;
130            default:
131                // handle switch/zoom here, as group will contain parameters and switch will not work
132                if (PATTERN_ZOOM.matcher("{" + matcher.group(1) + "}").matches()) {
133                    replacement = Integer.toString((inverse_zoom ? -1 * zoom : zoom) + zoom_offset);
134                } else if (PATTERN_SWITCH.matcher("{" + matcher.group(1) + "}").matches()) {
135                    replacement = randomParts[rand.nextInt(randomParts.length)];
136                } else {
137                    replacement = '{' + matcher.group(1) + '}';
138                }
139            }
140            matcher.appendReplacement(url, replacement);
141        }
142        matcher.appendTail(url);
143        return url.toString().replace(" ", "%20");
144    }
145
146    /**
147     * Checks if url is acceptable by this Tile Source
148     * @param url URL to check
149     */
150    public static void checkUrl(String url) {
151        assert url != null && !"".equals(url) : "URL cannot be null or empty";
152        Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);
153        while (m.find()) {
154            boolean isSupportedPattern = false;
155            for (Pattern pattern : ALL_PATTERNS) {
156                if (pattern.matcher(m.group()).matches()) {
157                    isSupportedPattern = true;
158                    break;
159                }
160            }
161            if (!isSupportedPattern) {
162                throw new IllegalArgumentException(
163                        m.group() + " is not a valid TMS argument. Please check this server URL:\n" + url);
164            }
165        }
166    }
167}