// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.data.imagery; import static org.openstreetmap.josm.tools.I18n.tr; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.coor.EastNorth; import org.openstreetmap.josm.data.projection.Projection; import org.openstreetmap.josm.gui.layer.WMSLayer; import org.openstreetmap.josm.tools.CheckParameterUtil; /** * Tile Source handling WMS providers * * @author Wiktor Niesiobędzki * @since 8526 */ public class TemplatedWMSTileSource extends AbstractWMSTileSource implements TemplatedTileSource { private final Map headers = new ConcurrentHashMap<>(); private final Set serverProjections; // CHECKSTYLE.OFF: SingleSpaceSeparator private static final Pattern PATTERN_HEADER = Pattern.compile("\\{header\\(([^,]+),([^}]+)\\)\\}"); private static final Pattern PATTERN_PROJ = Pattern.compile("\\{proj\\}"); private static final Pattern PATTERN_WKID = Pattern.compile("\\{wkid\\}"); private static final Pattern PATTERN_BBOX = Pattern.compile("\\{bbox\\}"); private static final Pattern PATTERN_W = Pattern.compile("\\{w\\}"); private static final Pattern PATTERN_S = Pattern.compile("\\{s\\}"); private static final Pattern PATTERN_E = Pattern.compile("\\{e\\}"); private static final Pattern PATTERN_N = Pattern.compile("\\{n\\}"); private static final Pattern PATTERN_WIDTH = Pattern.compile("\\{width\\}"); private static final Pattern PATTERN_HEIGHT = Pattern.compile("\\{height\\}"); private static final Pattern PATTERN_PARAM = Pattern.compile("\\{([^}]+)\\}"); // CHECKSTYLE.ON: SingleSpaceSeparator private static final NumberFormat latLonFormat = new DecimalFormat("###0.0000000", new DecimalFormatSymbols(Locale.US)); private static final Pattern[] ALL_PATTERNS = { PATTERN_HEADER, PATTERN_PROJ, PATTERN_WKID, PATTERN_BBOX, PATTERN_W, PATTERN_S, PATTERN_E, PATTERN_N, PATTERN_WIDTH, PATTERN_HEIGHT }; /** * Creates a tile source based on imagery info * @param info imagery info * @param tileProjection the tile projection */ public TemplatedWMSTileSource(ImageryInfo info, Projection tileProjection) { super(info, tileProjection); this.serverProjections = new TreeSet<>(info.getServerProjections()); handleTemplate(); initProjection(); } @Override public int getDefaultTileSize() { return WMSLayer.PROP_IMAGE_SIZE.get(); } @Override public String getTileUrl(int zoom, int tilex, int tiley) { String myProjCode = getServerCRS(); EastNorth nw = getTileEastNorth(tilex, tiley, zoom); EastNorth se = getTileEastNorth(tilex + 1, tiley + 1, zoom); double w = nw.getX(); double n = nw.getY(); double s = se.getY(); double e = se.getX(); if ("EPSG:4326".equals(myProjCode) && !serverProjections.contains(myProjCode) && serverProjections.contains("CRS:84")) { myProjCode = "CRS:84"; } // Bounding box coordinates have to be switched for WMS 1.3.0 EPSG:4326. // // Background: // // bbox=x_min,y_min,x_max,y_max // // SRS=... is WMS 1.1.1 // CRS=... is WMS 1.3.0 // // The difference: // For SRS x is east-west and y is north-south // For CRS x and y are as specified by the EPSG // E.g. [1] lists lat as first coordinate axis and lot as second, so it is switched for EPSG:4326. // For most other EPSG code there seems to be no difference. // CHECKSTYLE.OFF: LineLength // [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 // CHECKSTYLE.ON: LineLength boolean switchLatLon = false; if (baseUrl.toLowerCase(Locale.US).contains("crs=epsg:4326")) { switchLatLon = true; } else if (baseUrl.toLowerCase(Locale.US).contains("crs=")) { // assume WMS 1.3.0 switchLatLon = Main.getProjection().switchXY(); } String bbox; if (switchLatLon) { bbox = String.format("%s,%s,%s,%s", latLonFormat.format(s), latLonFormat.format(w), latLonFormat.format(n), latLonFormat.format(e)); } else { bbox = String.format("%s,%s,%s,%s", latLonFormat.format(w), latLonFormat.format(s), latLonFormat.format(e), latLonFormat.format(n)); } // Using StringBuffer and generic PATTERN_PARAM matcher gives 2x performance improvement over replaceAll StringBuffer url = new StringBuffer(baseUrl.length()); Matcher matcher = PATTERN_PARAM.matcher(baseUrl); while (matcher.find()) { String replacement; switch (matcher.group(1)) { case "proj": replacement = myProjCode; break; case "wkid": replacement = myProjCode.startsWith("EPSG:") ? myProjCode.substring(5) : myProjCode; break; case "bbox": replacement = bbox; break; case "w": replacement = latLonFormat.format(w); break; case "s": replacement = latLonFormat.format(s); break; case "e": replacement = latLonFormat.format(e); break; case "n": replacement = latLonFormat.format(n); break; case "width": case "height": replacement = String.valueOf(getTileSize()); break; default: replacement = '{' + matcher.group(1) + '}'; } matcher.appendReplacement(url, replacement); } matcher.appendTail(url); return url.toString().replace(" ", "%20"); } @Override public String getTileId(int zoom, int tilex, int tiley) { return getTileUrl(zoom, tilex, tiley); } @Override public Map getHeaders() { return headers; } /** * Checks if url is acceptable by this Tile Source * @param url URL to check */ public static void checkUrl(String url) { CheckParameterUtil.ensureParameterNotNull(url, "url"); Matcher m = PATTERN_PARAM.matcher(url); while (m.find()) { boolean isSupportedPattern = false; for (Pattern pattern : ALL_PATTERNS) { if (pattern.matcher(m.group()).matches()) { isSupportedPattern = true; break; } } if (!isSupportedPattern) { throw new IllegalArgumentException( tr("{0} is not a valid WMS argument. Please check this server URL:\n{1}", m.group(), url)); } } } private void handleTemplate() { // Capturing group pattern on switch values StringBuffer output = new StringBuffer(); Matcher matcher = PATTERN_HEADER.matcher(this.baseUrl); while (matcher.find()) { headers.put(matcher.group(1), matcher.group(2)); matcher.appendReplacement(output, ""); } matcher.appendTail(output); this.baseUrl = output.toString(); } }