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

Last change on this file since 8704 was 8704, checked in by wiktorn, 9 years ago

Fix minimum zooming breaking the calculation of best-zoom and zooming in.

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