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