source: josm/trunk/src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java@ 9167

Last change on this file since 9167 was 9167, checked in by wiktorn, 8 years ago
  • Move TemplatedWMSTileSource to Projection.getWorldBoundsBoxEastNorth
  • fix failing tests - move to EastNorth computations instead of LatLon,

which might be non-square, which lead to trouble checking, whether point was
within the bounds or not

See #12186

  • Property svn:eol-style set to native
File size: 13.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.imagery;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Point;
7import java.text.DecimalFormat;
8import java.text.DecimalFormatSymbols;
9import java.text.NumberFormat;
10import java.util.Locale;
11import java.util.Map;
12import java.util.Set;
13import java.util.TreeSet;
14import java.util.concurrent.ConcurrentHashMap;
15import java.util.regex.Matcher;
16import java.util.regex.Pattern;
17
18import org.openstreetmap.gui.jmapviewer.Tile;
19import org.openstreetmap.gui.jmapviewer.TileXY;
20import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
21import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
22import org.openstreetmap.gui.jmapviewer.tilesources.TMSTileSource;
23import org.openstreetmap.josm.Main;
24import org.openstreetmap.josm.data.ProjectionBounds;
25import org.openstreetmap.josm.data.coor.EastNorth;
26import org.openstreetmap.josm.data.coor.LatLon;
27import org.openstreetmap.josm.data.projection.Projection;
28import org.openstreetmap.josm.gui.layer.WMSLayer;
29import org.openstreetmap.josm.tools.CheckParameterUtil;
30
31/**
32 * Tile Source handling WMS providers
33 *
34 * @author Wiktor Niesiobędzki
35 * @since 8526
36 */
37public class TemplatedWMSTileSource extends TMSTileSource implements TemplatedTileSource {
38 private final Map<String, String> headers = new ConcurrentHashMap<>();
39 private final Set<String> serverProjections;
40 private EastNorth topLeftCorner;
41 private int[] tileXMax;
42 private int[] tileYMax;
43 private double[] degreesPerTile;
44
45 private static final Pattern PATTERN_HEADER = Pattern.compile("\\{header\\(([^,]+),([^}]+)\\)\\}");
46 private static final Pattern PATTERN_PROJ = Pattern.compile("\\{proj\\}");
47 private static final Pattern PATTERN_BBOX = Pattern.compile("\\{bbox\\}");
48 private static final Pattern PATTERN_W = Pattern.compile("\\{w\\}");
49 private static final Pattern PATTERN_S = Pattern.compile("\\{s\\}");
50 private static final Pattern PATTERN_E = Pattern.compile("\\{e\\}");
51 private static final Pattern PATTERN_N = Pattern.compile("\\{n\\}");
52 private static final Pattern PATTERN_WIDTH = Pattern.compile("\\{width\\}");
53 private static final Pattern PATTERN_HEIGHT = Pattern.compile("\\{height\\}");
54 private static final Pattern PATTERN_PARAM = Pattern.compile("\\{([^}]+)\\}");
55
56 private static final NumberFormat latLonFormat = new DecimalFormat("###0.0000000", new DecimalFormatSymbols(Locale.US));
57
58 private static final Pattern[] ALL_PATTERNS = {
59 PATTERN_HEADER, PATTERN_PROJ, PATTERN_BBOX, PATTERN_W, PATTERN_S, PATTERN_E, PATTERN_N, PATTERN_WIDTH, PATTERN_HEIGHT
60 };
61
62 /*
63 * Constant taken from OGC WMTS Implementation Specification (http://www.opengeospatial.org/standards/wmts)
64 * From table E.4 - Definition of Well-known scale set GoogleMapsCompatibile
65 *
66 * As higher zoom levels have denominator divided by 2, we keep only zoom level 1 in the code
67 */
68 private static final float SCALE_DENOMINATOR_ZOOM_LEVEL_1 = 559082264.0287178f;
69
70 /**
71 * Creates a tile source based on imagery info
72 * @param info imagery info
73 */
74 public TemplatedWMSTileSource(ImageryInfo info) {
75 super(info);
76 this.serverProjections = new TreeSet<>(info.getServerProjections());
77 handleTemplate();
78 initProjection();
79 // FIXME: remove in September 2015, when ImageryPreferenceEntry.tileSize will be initialized to -1 instead to 256
80 // need to leave it as it is to keep compatibility between tested and latest JOSM versions
81 tileSize = WMSLayer.PROP_IMAGE_SIZE.get();
82 }
83
84 /**
85 * Initializes class with current projection in JOSM. This call is needed every time projection changes.
86 */
87 public void initProjection() {
88 initProjection(Main.getProjection());
89 }
90
91 /**
92 * Initializes class with projection in JOSM. This call is needed every time projection changes.
93 * @param proj new projection that shall be used for computations
94 */
95 public void initProjection(Projection proj) {
96 ProjectionBounds worldBounds = proj.getWorldBoundsBoxEastNorth();
97 EastNorth min = worldBounds.getMin();
98 EastNorth max = worldBounds.getMax();
99 this.topLeftCorner = new EastNorth(min.east(), max.north());
100
101 EastNorth bottomRight = new EastNorth(worldBounds.getMax().east(), worldBounds.getMin().north());
102
103 // use 256 as "tile size" to keep the scale in line with default tiles in Mercator projection
104 double crsScale = 256 * 0.28e-03 / proj.getMetersPerUnit();
105 tileXMax = new int[getMaxZoom() + 1];
106 tileYMax = new int[getMaxZoom() + 1];
107 degreesPerTile = new double[getMaxZoom() + 1];
108
109 for (int zoom = 1; zoom <= getMaxZoom(); zoom++) {
110 // use well known scale set "GoogleCompatibile" from OGC WMTS spec to calculate number of tiles per zoom level
111 // this makes the zoom levels "glued" to standard TMS zoom levels
112 degreesPerTile[zoom] = (SCALE_DENOMINATOR_ZOOM_LEVEL_1 / Math.pow(2, zoom - 1)) * crsScale;
113 TileXY maxTileIndex = eastNorthToTileXY(bottomRight, zoom);
114 tileXMax[zoom] = maxTileIndex.getXIndex();
115 tileYMax[zoom] = maxTileIndex.getYIndex();
116 }
117 }
118
119 @Override
120 public int getDefaultTileSize() {
121 return WMSLayer.PROP_IMAGE_SIZE.get();
122 }
123
124 @Override
125 public String getTileUrl(int zoom, int tilex, int tiley) {
126 String myProjCode = Main.getProjection().toCode();
127
128 EastNorth nw = getTileEastNorth(tilex, tiley, zoom);
129 EastNorth se = getTileEastNorth(tilex + 1, tiley + 1, zoom);
130
131 double w = nw.getX();
132 double n = nw.getY();
133
134 double s = se.getY();
135 double e = se.getX();
136
137 if (!serverProjections.contains(myProjCode) && serverProjections.contains("EPSG:4326") && "EPSG:3857".equals(myProjCode)) {
138 LatLon swll = Main.getProjection().eastNorth2latlon(new EastNorth(w, s));
139 LatLon nell = Main.getProjection().eastNorth2latlon(new EastNorth(e, n));
140 myProjCode = "EPSG:4326";
141 s = swll.lat();
142 w = swll.lon();
143 n = nell.lat();
144 e = nell.lon();
145 }
146
147 if ("EPSG:4326".equals(myProjCode) && !serverProjections.contains(myProjCode) && serverProjections.contains("CRS:84")) {
148 myProjCode = "CRS:84";
149 }
150
151 // Bounding box coordinates have to be switched for WMS 1.3.0 EPSG:4326.
152 //
153 // Background:
154 //
155 // bbox=x_min,y_min,x_max,y_max
156 //
157 // SRS=... is WMS 1.1.1
158 // CRS=... is WMS 1.3.0
159 //
160 // The difference:
161 // For SRS x is east-west and y is north-south
162 // For CRS x and y are as specified by the EPSG
163 // E.g. [1] lists lat as first coordinate axis and lot as second, so it is switched for EPSG:4326.
164 // For most other EPSG code there seems to be no difference.
165 // CHECKSTYLE.OFF: LineLength
166 // [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
167 // CHECKSTYLE.ON: LineLength
168 boolean switchLatLon = false;
169 if (baseUrl.toLowerCase(Locale.US).contains("crs=epsg:4326")) {
170 switchLatLon = true;
171 } else if (baseUrl.toLowerCase(Locale.US).contains("crs=")) {
172 // assume WMS 1.3.0
173 switchLatLon = Main.getProjection().switchXY();
174 }
175 String bbox;
176 if (switchLatLon) {
177 bbox = String.format("%s,%s,%s,%s", latLonFormat.format(s), latLonFormat.format(w), latLonFormat.format(n), latLonFormat.format(e));
178 } else {
179 bbox = String.format("%s,%s,%s,%s", latLonFormat.format(w), latLonFormat.format(s), latLonFormat.format(e), latLonFormat.format(n));
180 }
181
182 // Using StringBuffer and generic PATTERN_PARAM matcher gives 2x performance improvement over replaceAll
183 StringBuffer url = new StringBuffer(baseUrl.length());
184 Matcher matcher = PATTERN_PARAM.matcher(baseUrl);
185 while (matcher.find()) {
186 String replacement;
187 switch (matcher.group(1)) {
188 case "proj":
189 replacement = myProjCode;
190 break;
191 case "bbox":
192 replacement = bbox;
193 break;
194 case "w":
195 replacement = latLonFormat.format(w);
196 break;
197 case "s":
198 replacement = latLonFormat.format(s);
199 break;
200 case "e":
201 replacement = latLonFormat.format(e);
202 break;
203 case "n":
204 replacement = latLonFormat.format(n);
205 break;
206 case "width":
207 case "height":
208 replacement = String.valueOf(getTileSize());
209 break;
210 default:
211 replacement = '{' + matcher.group(1) + '}';
212 }
213 matcher.appendReplacement(url, replacement);
214 }
215 matcher.appendTail(url);
216 return url.toString().replace(" ", "%20");
217 }
218
219 @Override
220 public String getTileId(int zoom, int tilex, int tiley) {
221 return getTileUrl(zoom, tilex, tiley);
222 }
223
224 @Override
225 public ICoordinate tileXYToLatLon(Tile tile) {
226 return tileXYToLatLon(tile.getXtile(), tile.getYtile(), tile.getZoom());
227 }
228
229 @Override
230 public ICoordinate tileXYToLatLon(TileXY xy, int zoom) {
231 return tileXYToLatLon(xy.getXIndex(), xy.getYIndex(), zoom);
232 }
233
234 @Override
235 public ICoordinate tileXYToLatLon(int x, int y, int zoom) {
236 return Main.getProjection().eastNorth2latlon(getTileEastNorth(x, y, zoom)).toCoordinate();
237 }
238
239 @Override
240 public TileXY latLonToTileXY(double lat, double lon, int zoom) {
241 Projection proj = Main.getProjection();
242 EastNorth enPoint = proj.latlon2eastNorth(new LatLon(lat, lon));
243 return eastNorthToTileXY(enPoint, zoom);
244 }
245
246 private TileXY eastNorthToTileXY(EastNorth enPoint, int zoom) {
247 double scale = getDegreesPerTile(zoom);
248 return new TileXY(
249 (enPoint.east() - topLeftCorner.east()) / scale,
250 (topLeftCorner.north() - enPoint.north()) / scale
251 );
252 }
253
254 @Override
255 public TileXY latLonToTileXY(ICoordinate point, int zoom) {
256 return latLonToTileXY(point.getLat(), point.getLon(), zoom);
257 }
258
259 @Override
260 public int getTileXMax(int zoom) {
261 return tileXMax[zoom];
262 }
263
264 @Override
265 public int getTileXMin(int zoom) {
266 return 0;
267 }
268
269 @Override
270 public int getTileYMax(int zoom) {
271 return tileYMax[zoom];
272 }
273
274 @Override
275 public int getTileYMin(int zoom) {
276 return 0;
277 }
278
279 @Override
280 public Point latLonToXY(double lat, double lon, int zoom) {
281 double scale = getDegreesPerTile(zoom) / getTileSize();
282 EastNorth point = Main.getProjection().latlon2eastNorth(new LatLon(lat, lon));
283 return new Point(
284 (int) Math.round((point.east() - topLeftCorner.east()) / scale),
285 (int) Math.round((topLeftCorner.north() - point.north()) / scale)
286 );
287 }
288
289 @Override
290 public Point latLonToXY(ICoordinate point, int zoom) {
291 return latLonToXY(point.getLat(), point.getLon(), zoom);
292 }
293
294 @Override
295 public ICoordinate xyToLatLon(Point point, int zoom) {
296 return xyToLatLon(point.x, point.y, zoom);
297 }
298
299 @Override
300 public ICoordinate xyToLatLon(int x, int y, int zoom) {
301 double scale = getDegreesPerTile(zoom) / getTileSize();
302 Projection proj = Main.getProjection();
303 EastNorth ret = new EastNorth(
304 topLeftCorner.east() + x * scale,
305 topLeftCorner.north() - y * scale
306 );
307 return proj.eastNorth2latlon(ret).toCoordinate();
308 }
309
310 @Override
311 public Map<String, String> getHeaders() {
312 return headers;
313 }
314
315 /**
316 * Checks if url is acceptable by this Tile Source
317 * @param url URL to check
318 */
319 public static void checkUrl(String url) {
320 CheckParameterUtil.ensureParameterNotNull(url, "url");
321 Matcher m = PATTERN_PARAM.matcher(url);
322 while (m.find()) {
323 boolean isSupportedPattern = false;
324 for (Pattern pattern : ALL_PATTERNS) {
325 if (pattern.matcher(m.group()).matches()) {
326 isSupportedPattern = true;
327 break;
328 }
329 }
330 if (!isSupportedPattern) {
331 throw new IllegalArgumentException(
332 tr("{0} is not a valid WMS argument. Please check this server URL:\n{1}", m.group(), url));
333 }
334 }
335 }
336
337 private void handleTemplate() {
338 // Capturing group pattern on switch values
339 StringBuffer output = new StringBuffer();
340 Matcher matcher = PATTERN_HEADER.matcher(this.baseUrl);
341 while (matcher.find()) {
342 headers.put(matcher.group(1), matcher.group(2));
343 matcher.appendReplacement(output, "");
344 }
345 matcher.appendTail(output);
346 this.baseUrl = output.toString();
347 }
348
349 protected EastNorth getTileEastNorth(int x, int y, int z) {
350 double scale = getDegreesPerTile(z);
351 return new EastNorth(
352 topLeftCorner.east() + x * scale,
353 topLeftCorner.north() - y * scale
354 );
355 }
356
357 private double getDegreesPerTile(int zoom) {
358 return degreesPerTile[zoom];
359 }
360}
Note: See TracBrowser for help on using the repository browser.