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

Last change on this file since 10181 was 10181, checked in by Don-vip, 8 years ago

sonar - squid:S2184 - Math operands should be cast before assignment

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