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}