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.Arrays; 010import java.util.Locale; 011import java.util.Map; 012import java.util.Set; 013import java.util.TreeSet; 014import java.util.concurrent.ConcurrentHashMap; 015import java.util.regex.Matcher; 016import java.util.regex.Pattern; 017 018import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource; 019import org.openstreetmap.josm.data.coor.EastNorth; 020import org.openstreetmap.josm.data.projection.Projection; 021import org.openstreetmap.josm.data.projection.ProjectionRegistry; 022import org.openstreetmap.josm.gui.layer.WMSLayer; 023import org.openstreetmap.josm.tools.CheckParameterUtil; 024import org.openstreetmap.josm.tools.Utils; 025 026/** 027 * Tile Source handling WMS providers 028 * 029 * @author Wiktor Niesiobędzki 030 * @since 8526 031 */ 032public class TemplatedWMSTileSource extends AbstractWMSTileSource implements TemplatedTileSource { 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_TIME = Pattern.compile("\\{time\\}"); // Sentinel-2 045 private static final Pattern PATTERN_PARAM = Pattern.compile("\\{([^}]+)\\}"); 046 // CHECKSTYLE.ON: SingleSpaceSeparator 047 048 private static final NumberFormat LATLON_FORMAT = new DecimalFormat("###0.0000000", new DecimalFormatSymbols(Locale.US)); 049 050 private static final Pattern[] ALL_PATTERNS = { 051 PATTERN_HEADER, PATTERN_PROJ, PATTERN_WKID, PATTERN_BBOX, 052 PATTERN_W, PATTERN_S, PATTERN_E, PATTERN_N, 053 PATTERN_WIDTH, PATTERN_HEIGHT, PATTERN_TIME, 054 }; 055 056 private final Set<String> serverProjections; 057 private final Map<String, String> headers = new ConcurrentHashMap<>(); 058 private final String date; 059 private final boolean switchLatLon; 060 061 /** 062 * Creates a tile source based on imagery info 063 * @param info imagery info 064 * @param tileProjection the tile projection 065 */ 066 public TemplatedWMSTileSource(ImageryInfo info, Projection tileProjection) { 067 super(info, tileProjection); 068 this.serverProjections = new TreeSet<>(info.getServerProjections()); 069 this.headers.putAll(info.getCustomHttpHeaders()); 070 this.date = info.getDate(); 071 handleTemplate(); 072 initProjection(); 073 // Bounding box coordinates have to be switched for WMS 1.3.0 EPSG:4326. 074 // 075 // Background: 076 // 077 // bbox=x_min,y_min,x_max,y_max 078 // 079 // SRS=... is WMS 1.1.1 080 // CRS=... is WMS 1.3.0 081 // 082 // The difference: 083 // For SRS x is east-west and y is north-south 084 // For CRS x and y are as specified by the EPSG 085 // E.g. [1] lists lat as first coordinate axis and lot as second, so it is switched for EPSG:4326. 086 // For most other EPSG code there seems to be no difference. 087 // CHECKSTYLE.OFF: LineLength 088 // [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 089 // CHECKSTYLE.ON: LineLength 090 if (baseUrl.toLowerCase(Locale.US).contains("crs=epsg:4326")) { 091 switchLatLon = true; 092 } else if (baseUrl.toLowerCase(Locale.US).contains("crs=")) { 093 // assume WMS 1.3.0 094 switchLatLon = ProjectionRegistry.getProjection().switchXY(); 095 } else { 096 switchLatLon = false; 097 } 098 } 099 100 @Override 101 public int getDefaultTileSize() { 102 return WMSLayer.PROP_IMAGE_SIZE.get(); 103 } 104 105 @Override 106 public String getTileUrl(int zoom, int tilex, int tiley) { 107 String myProjCode = getServerCRS(); 108 109 EastNorth nw = getTileEastNorth(tilex, tiley, zoom); 110 EastNorth se = getTileEastNorth(tilex + 1, tiley + 1, zoom); 111 112 double w = nw.getX(); 113 double n = nw.getY(); 114 115 double s = se.getY(); 116 double e = se.getX(); 117 118 if ("EPSG:4326".equals(myProjCode) && !serverProjections.contains(myProjCode) && serverProjections.contains("CRS:84")) { 119 myProjCode = "CRS:84"; 120 } 121 122 // Using StringBuffer and generic PATTERN_PARAM matcher gives 2x performance improvement over replaceAll 123 StringBuffer url = new StringBuffer(baseUrl.length()); 124 Matcher matcher = PATTERN_PARAM.matcher(baseUrl); 125 while (matcher.find()) { 126 String replacement; 127 switch (matcher.group(1)) { 128 case "proj": 129 replacement = myProjCode; 130 break; 131 case "wkid": 132 replacement = myProjCode.startsWith("EPSG:") ? myProjCode.substring(5) : myProjCode; 133 break; 134 case "bbox": 135 replacement = getBbox(zoom, tilex, tiley, switchLatLon); 136 break; 137 case "w": 138 replacement = LATLON_FORMAT.format(w); 139 break; 140 case "s": 141 replacement = LATLON_FORMAT.format(s); 142 break; 143 case "e": 144 replacement = LATLON_FORMAT.format(e); 145 break; 146 case "n": 147 replacement = LATLON_FORMAT.format(n); 148 break; 149 case "width": 150 case "height": 151 replacement = String.valueOf(getTileSize()); 152 break; 153 case "time": 154 replacement = Utils.encodeUrl(date); 155 break; 156 default: 157 replacement = '{' + matcher.group(1) + '}'; 158 } 159 matcher.appendReplacement(url, replacement); 160 } 161 matcher.appendTail(url); 162 return url.toString().replace(" ", "%20"); 163 } 164 165 @Override 166 public String getTileId(int zoom, int tilex, int tiley) { 167 return getTileUrl(zoom, tilex, tiley); 168 } 169 170 @Override 171 public Map<String, String> getHeaders() { 172 return headers; 173 } 174 175 /** 176 * Checks if url is acceptable by this Tile Source 177 * @param url URL to check 178 */ 179 public static void checkUrl(String url) { 180 CheckParameterUtil.ensureParameterNotNull(url, "url"); 181 Matcher m = PATTERN_PARAM.matcher(url); 182 while (m.find()) { 183 boolean isSupportedPattern = Arrays.stream(ALL_PATTERNS) 184 .anyMatch(pattern -> pattern.matcher(m.group()).matches()); 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}