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.coor.LatLon; 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 latLonFormat = 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 */ 057 public TemplatedWMSTileSource(ImageryInfo info) { 058 super(info); 059 this.serverProjections = new TreeSet<>(info.getServerProjections()); 060 handleTemplate(); 061 initProjection(); 062 } 063 064 @Override 065 public int getDefaultTileSize() { 066 return WMSLayer.PROP_IMAGE_SIZE.get(); 067 } 068 069 @Override 070 public String getTileUrl(int zoom, int tilex, int tiley) { 071 String myProjCode = Main.getProjection().toCode(); 072 073 EastNorth nw = getTileEastNorth(tilex, tiley, zoom); 074 EastNorth se = getTileEastNorth(tilex + 1, tiley + 1, zoom); 075 076 double w = nw.getX(); 077 double n = nw.getY(); 078 079 double s = se.getY(); 080 double e = se.getX(); 081 082 if (!serverProjections.contains(myProjCode) && serverProjections.contains("EPSG:4326") && "EPSG:3857".equals(myProjCode)) { 083 LatLon swll = Main.getProjection().eastNorth2latlon(new EastNorth(w, s)); 084 LatLon nell = Main.getProjection().eastNorth2latlon(new EastNorth(e, n)); 085 myProjCode = "EPSG:4326"; 086 s = swll.lat(); 087 w = swll.lon(); 088 n = nell.lat(); 089 e = nell.lon(); 090 } 091 092 if ("EPSG:4326".equals(myProjCode) && !serverProjections.contains(myProjCode) && serverProjections.contains("CRS:84")) { 093 myProjCode = "CRS:84"; 094 } 095 096 // Bounding box coordinates have to be switched for WMS 1.3.0 EPSG:4326. 097 // 098 // Background: 099 // 100 // bbox=x_min,y_min,x_max,y_max 101 // 102 // SRS=... is WMS 1.1.1 103 // CRS=... is WMS 1.3.0 104 // 105 // The difference: 106 // For SRS x is east-west and y is north-south 107 // For CRS x and y are as specified by the EPSG 108 // E.g. [1] lists lat as first coordinate axis and lot as second, so it is switched for EPSG:4326. 109 // For most other EPSG code there seems to be no difference. 110 // CHECKSTYLE.OFF: LineLength 111 // [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 112 // CHECKSTYLE.ON: LineLength 113 boolean switchLatLon = false; 114 if (baseUrl.toLowerCase(Locale.US).contains("crs=epsg:4326")) { 115 switchLatLon = true; 116 } else if (baseUrl.toLowerCase(Locale.US).contains("crs=")) { 117 // assume WMS 1.3.0 118 switchLatLon = Main.getProjection().switchXY(); 119 } 120 String bbox; 121 if (switchLatLon) { 122 bbox = String.format("%s,%s,%s,%s", latLonFormat.format(s), latLonFormat.format(w), latLonFormat.format(n), latLonFormat.format(e)); 123 } else { 124 bbox = String.format("%s,%s,%s,%s", latLonFormat.format(w), latLonFormat.format(s), latLonFormat.format(e), latLonFormat.format(n)); 125 } 126 127 // Using StringBuffer and generic PATTERN_PARAM matcher gives 2x performance improvement over replaceAll 128 StringBuffer url = new StringBuffer(baseUrl.length()); 129 Matcher matcher = PATTERN_PARAM.matcher(baseUrl); 130 while (matcher.find()) { 131 String replacement; 132 switch (matcher.group(1)) { 133 case "proj": 134 replacement = myProjCode; 135 break; 136 case "wkid": 137 replacement = myProjCode.startsWith("EPSG:") ? myProjCode.substring(5) : myProjCode; 138 break; 139 case "bbox": 140 replacement = bbox; 141 break; 142 case "w": 143 replacement = latLonFormat.format(w); 144 break; 145 case "s": 146 replacement = latLonFormat.format(s); 147 break; 148 case "e": 149 replacement = latLonFormat.format(e); 150 break; 151 case "n": 152 replacement = latLonFormat.format(n); 153 break; 154 case "width": 155 case "height": 156 replacement = String.valueOf(getTileSize()); 157 break; 158 default: 159 replacement = '{' + matcher.group(1) + '}'; 160 } 161 matcher.appendReplacement(url, replacement); 162 } 163 matcher.appendTail(url); 164 return url.toString().replace(" ", "%20"); 165 } 166 167 @Override 168 public String getTileId(int zoom, int tilex, int tiley) { 169 return getTileUrl(zoom, tilex, tiley); 170 } 171 172 @Override 173 public Map<String, String> getHeaders() { 174 return headers; 175 } 176 177 /** 178 * Checks if url is acceptable by this Tile Source 179 * @param url URL to check 180 */ 181 public static void checkUrl(String url) { 182 CheckParameterUtil.ensureParameterNotNull(url, "url"); 183 Matcher m = PATTERN_PARAM.matcher(url); 184 while (m.find()) { 185 boolean isSupportedPattern = false; 186 for (Pattern pattern : ALL_PATTERNS) { 187 if (pattern.matcher(m.group()).matches()) { 188 isSupportedPattern = true; 189 break; 190 } 191 } 192 if (!isSupportedPattern) { 193 throw new IllegalArgumentException( 194 tr("{0} is not a valid WMS argument. Please check this server URL:\n{1}", m.group(), url)); 195 } 196 } 197 } 198 199 private void handleTemplate() { 200 // Capturing group pattern on switch values 201 StringBuffer output = new StringBuffer(); 202 Matcher matcher = PATTERN_HEADER.matcher(this.baseUrl); 203 while (matcher.find()) { 204 headers.put(matcher.group(1), matcher.group(2)); 205 matcher.appendReplacement(output, ""); 206 } 207 matcher.appendTail(output); 208 this.baseUrl = output.toString(); 209 } 210}