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

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

checkstyle and sonar issues fixes

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