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