source: josm/trunk/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java@ 8585

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

Checkstyle fixes

File size: 23.0 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.Dimension;
7import java.awt.GridBagLayout;
8import java.awt.Point;
9import java.io.ByteArrayInputStream;
10import java.io.IOException;
11import java.io.InputStream;
12import java.net.MalformedURLException;
13import java.net.URL;
14import java.util.ArrayList;
15import java.util.Collection;
16import java.util.Comparator;
17import java.util.HashMap;
18import java.util.Map;
19import java.util.Set;
20import java.util.SortedSet;
21import java.util.TreeSet;
22import java.util.concurrent.ConcurrentHashMap;
23import java.util.regex.Matcher;
24import java.util.regex.Pattern;
25
26import javax.swing.JList;
27import javax.swing.JPanel;
28import javax.swing.ListSelectionModel;
29import javax.xml.namespace.QName;
30import javax.xml.parsers.DocumentBuilder;
31import javax.xml.parsers.DocumentBuilderFactory;
32import javax.xml.xpath.XPath;
33import javax.xml.xpath.XPathConstants;
34import javax.xml.xpath.XPathExpression;
35import javax.xml.xpath.XPathExpressionException;
36import javax.xml.xpath.XPathFactory;
37
38import org.openstreetmap.gui.jmapviewer.Coordinate;
39import org.openstreetmap.gui.jmapviewer.Tile;
40import org.openstreetmap.gui.jmapviewer.TileXY;
41import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
42import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
43import org.openstreetmap.gui.jmapviewer.tilesources.TMSTileSource;
44import org.openstreetmap.josm.Main;
45import org.openstreetmap.josm.data.Bounds;
46import org.openstreetmap.josm.data.coor.EastNorth;
47import org.openstreetmap.josm.data.coor.LatLon;
48import org.openstreetmap.josm.data.projection.Projection;
49import org.openstreetmap.josm.data.projection.Projections;
50import org.openstreetmap.josm.gui.ExtendedDialog;
51import org.openstreetmap.josm.io.CachedFile;
52import org.openstreetmap.josm.tools.CheckParameterUtil;
53import org.openstreetmap.josm.tools.GBC;
54import org.openstreetmap.josm.tools.Utils;
55import org.w3c.dom.DOMException;
56import org.w3c.dom.Document;
57import org.w3c.dom.Node;
58import org.w3c.dom.NodeList;
59
60/**
61 * Tile Source handling WMS providers
62 *
63 * @author Wiktor Niesiobędzki
64 * @since 8526
65 */
66public class WMTSTileSource extends TMSTileSource implements TemplatedTileSource {
67 private static final String PATTERN_HEADER = "\\{header\\(([^,]+),([^}]+)\\)\\}";
68
69 private static final String URL_GET_ENCODING_PARAMS = "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER={layer}&STYLE={Style}&"
70 + "FORMAT={format}&tileMatrixSet={TileMatrixSet}&tileMatrix={TileMatrix}&tileRow={TileRow}&tileCol={TileCol}";
71
72 private static final String[] ALL_PATTERNS = {
73 PATTERN_HEADER,
74 };
75
76 private static class TileMatrix {
77 String identifier;
78 double scaleDenominator;
79 EastNorth topLeftCorner;
80 int tileWidth;
81 int tileHeight;
82 }
83
84 private static class TileMatrixSet {
85 SortedSet<TileMatrix> tileMatrix = new TreeSet<>(new Comparator<TileMatrix>() {
86 @Override
87 public int compare(TileMatrix o1, TileMatrix o2) {
88 // reverse the order, so it will be from greatest (lowest zoom level) to lowest value (highest zoom level)
89 return -1 * Double.compare(o1.scaleDenominator, o2.scaleDenominator);
90 }
91 }); // sorted by zoom level
92 String crs;
93 String identifier;
94 }
95
96 private static class Layer {
97 String format;
98 String name;
99 Map<String, TileMatrixSet> tileMatrixSetByCRS = new ConcurrentHashMap<>();
100 public String baseUrl;
101 }
102
103 private enum TransferMode {
104 KVP("KVP"),
105 REST("RESTful");
106
107 private final String typeString;
108
109 private TransferMode(String urlString) {
110 this.typeString = urlString;
111 }
112
113 private String getTypeString() {
114 return typeString;
115 }
116
117 private static TransferMode fromString(String s) {
118 for (TransferMode type : TransferMode.values()) {
119 if (type.getTypeString().equals(s)) {
120 return type;
121 }
122 }
123 return null;
124 }
125 }
126
127 private static final class SelectLayerDialog extends ExtendedDialog {
128 private Layer[] layers;
129 private JList<String> list;
130
131 private SelectLayerDialog(Collection<Layer> layers) {
132 super(Main.parent, tr("Select WMTS layer"), new String[]{tr("Add layers"), tr("Cancel")});
133 this.layers = layers.toArray(new Layer[]{});
134 this.list = new JList<>(getLayerNames(layers));
135 this.list.setPreferredSize(new Dimension(400, 400));
136 this.list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
137 JPanel panel = new JPanel(new GridBagLayout());
138 panel.add(this.list, GBC.eol().fill());
139 setContent(panel);
140 }
141
142 private String[] getLayerNames(Collection<Layer> layers) {
143 Collection<String> ret = new ArrayList<>();
144 for (Layer layer: layers) {
145 ret.add(layer.name);
146 }
147 return ret.toArray(new String[]{});
148 }
149
150 public Layer getSelectedLayer() {
151 int index = list.getSelectedIndex();
152 if (index < 0) {
153 return null; //nothing selected
154 }
155 return layers[index];
156 }
157 }
158
159 private Map<String, String> headers = new HashMap<>();
160 private Collection<Layer> layers;
161 private Layer currentLayer;
162 private TileMatrixSet currentTileMatrixSet;
163 private double crsScale;
164 private TransferMode transferMode;
165 private String style = "";
166
167 /**
168 * Creates a tile source based on imagery info
169 * @param info imagery info
170 * @throws IOException if any I/O error occurs
171 */
172 public WMTSTileSource(ImageryInfo info) throws IOException {
173 super(info);
174 this.baseUrl = normalizeCapabilitiesUrl(handleTemplate(info.getUrl()));
175 this.layers = getCapabilities();
176 if (layers.size() > 1) {
177 SelectLayerDialog layerSelection = new SelectLayerDialog(layers);
178 if (layerSelection.showDialog().getValue() == 1) {
179 this.currentLayer = layerSelection.getSelectedLayer();
180 // TODO: save layer information into ImageryInfo / ImageryPreferences
181 } else {
182 throw new IllegalArgumentException(); //user canceled operation
183 }
184 } else if (layers.size() == 1) {
185 this.currentLayer = this.layers.iterator().next();
186 } else {
187 throw new IllegalArgumentException(tr("No layers defined by getCapabilities document: {0}", info.getUrl()));
188 }
189
190 initProjection();
191 }
192
193 private String handleTemplate(String url) {
194 Pattern pattern = Pattern.compile(PATTERN_HEADER);
195 StringBuffer output = new StringBuffer();
196 Matcher matcher = pattern.matcher(url);
197 while (matcher.find()) {
198 this.headers.put(matcher.group(1), matcher.group(2));
199 matcher.appendReplacement(output, "");
200 }
201 matcher.appendTail(output);
202 return output.toString();
203 }
204
205 private Collection<Layer> getCapabilities() throws IOException {
206 DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
207 builderFactory.setValidating(false);
208 builderFactory.setNamespaceAware(false);
209 DocumentBuilder builder = null;
210 InputStream in = new CachedFile(baseUrl).
211 setHttpHeaders(headers).
212 setMaxAge(7 * CachedFile.DAYS).
213 setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince).
214 getInputStream();
215 try {
216 builder = builderFactory.newDocumentBuilder();
217 byte[] data = Utils.readBytesFromStream(in);
218 if (data == null || data.length == 0) {
219 throw new IllegalArgumentException("Could not read data from: " + baseUrl);
220 }
221 Document document = builder.parse(new ByteArrayInputStream(data));
222 Node getTileOperation = getByXpath(document,
223 "/Capabilities/OperationsMetadata/Operation[@name=\"GetTile\"]/DCP/HTTP/Get").item(0);
224 this.baseUrl = getStringByXpath(getTileOperation, "@href");
225 this.transferMode = TransferMode.fromString(getStringByXpath(getTileOperation,
226 "Constraint[@name=\"GetEncoding\"]/AllowedValues/Value"));
227 NodeList layersNodeList = getByXpath(document, "/Capabilities/Contents/Layer");
228 Map<String, TileMatrixSet> matrixSetById = parseMatrices(getByXpath(document, "/Capabilities/Contents/TileMatrixSet"));
229 return parseLayer(layersNodeList, matrixSetById);
230
231 } catch (Exception e) {
232 throw new IllegalArgumentException(e);
233 }
234 }
235
236 private static String normalizeCapabilitiesUrl(String url) throws MalformedURLException {
237 URL inUrl = new URL(url);
238 URL ret = new URL(inUrl.getProtocol(), inUrl.getHost(), inUrl.getPort(), inUrl.getFile());
239 return ret.toExternalForm();
240 }
241
242 private Collection<Layer> parseLayer(NodeList nodeList, Map<String, TileMatrixSet> matrixSetById) throws XPathExpressionException {
243 Collection<Layer> ret = new ArrayList<>();
244 for (int layerId = 0; layerId < nodeList.getLength(); layerId++) {
245 Node layerNode = nodeList.item(layerId);
246 Layer layer = new Layer();
247 layer.format = getStringByXpath(layerNode, "Format");
248 layer.name = getStringByXpath(layerNode, "Identifier");
249 layer.baseUrl = getStringByXpath(layerNode, "ResourceURL[@resourceType='tile']/@template");
250 NodeList tileMatrixSetLinks = getByXpath(layerNode, "TileMatrixSetLink");
251 for (int tileMatrixId = 0; tileMatrixId < tileMatrixSetLinks.getLength(); tileMatrixId++) {
252 Node tileMatrixLink = tileMatrixSetLinks.item(tileMatrixId);
253 TileMatrixSet tms = matrixSetById.get(getStringByXpath(tileMatrixLink, "TileMatrixSet"));
254 layer.tileMatrixSetByCRS.put(tms.crs, tms);
255 }
256 ret.add(layer);
257 }
258 return ret;
259
260 }
261
262 private Map<String, TileMatrixSet> parseMatrices(NodeList nodeList) throws DOMException, XPathExpressionException {
263 Map<String, TileMatrixSet> ret = new ConcurrentHashMap<>();
264 for (int matrixSetId = 0; matrixSetId < nodeList.getLength(); matrixSetId++) {
265 Node matrixSetNode = nodeList.item(matrixSetId);
266 TileMatrixSet matrixSet = new TileMatrixSet();
267 matrixSet.identifier = getStringByXpath(matrixSetNode, "Identifier");
268 matrixSet.crs = crsToCode(getStringByXpath(matrixSetNode, "SupportedCRS"));
269 NodeList tileMatrixList = getByXpath(matrixSetNode, "TileMatrix");
270 Projection matrixProj = Projections.getProjectionByCode(matrixSet.crs);
271 if (matrixProj == null) {
272 // use current projection if none found. Maybe user is using custom string
273 matrixProj = Main.getProjection();
274 }
275 for (int matrixId = 0; matrixId < tileMatrixList.getLength(); matrixId++) {
276 Node tileMatrixNode = tileMatrixList.item(matrixId);
277 TileMatrix tileMatrix = new TileMatrix();
278 tileMatrix.identifier = getStringByXpath(tileMatrixNode, "Identifier");
279 tileMatrix.scaleDenominator = Double.parseDouble(getStringByXpath(tileMatrixNode, "ScaleDenominator"));
280 String[] topLeftCorner = getStringByXpath(tileMatrixNode, "TopLeftCorner").split(" ");
281
282 if (matrixProj.switchXY()) {
283 tileMatrix.topLeftCorner = new EastNorth(Double.parseDouble(topLeftCorner[1]), Double.parseDouble(topLeftCorner[0]));
284 } else {
285 tileMatrix.topLeftCorner = new EastNorth(Double.parseDouble(topLeftCorner[0]), Double.parseDouble(topLeftCorner[1]));
286 }
287 tileMatrix.tileHeight = Integer.parseInt(getStringByXpath(tileMatrixNode, "TileHeight"));
288 tileMatrix.tileWidth = Integer.parseInt(getStringByXpath(tileMatrixNode, "TileHeight"));
289 if (tileMatrix.tileHeight != tileMatrix.tileWidth) {
290 throw new AssertionError(tr("Only square tiles are supported. {0}x{1} returned by server for TileMatrix identifier {2}",
291 tileMatrix.tileHeight, tileMatrix.tileWidth, tileMatrix.identifier));
292 }
293
294 matrixSet.tileMatrix.add(tileMatrix);
295 }
296 ret.put(matrixSet.identifier, matrixSet);
297 }
298 return ret;
299 }
300
301 private static String crsToCode(String crsIdentifier) {
302 if (crsIdentifier.startsWith("urn:ogc:def:crs:")) {
303 return crsIdentifier.replaceFirst("urn:ogc:def:crs:([^:]*):.*:(.*)$", "$1:$2");
304 }
305 return crsIdentifier;
306 }
307
308 private static String getStringByXpath(Node document, String xpathQuery) throws XPathExpressionException {
309 return (String) getByXpath(document, xpathQuery, XPathConstants.STRING);
310 }
311
312 private static NodeList getByXpath(Node document, String xpathQuery) throws XPathExpressionException {
313 return (NodeList) getByXpath(document, xpathQuery, XPathConstants.NODESET);
314 }
315
316 private static Object getByXpath(Node document, String xpathQuery, QName returnType) throws XPathExpressionException {
317 XPath xpath = XPathFactory.newInstance().newXPath();
318 XPathExpression expr = xpath.compile(xpathQuery);
319 return expr.evaluate(document, returnType);
320 }
321
322 /**
323 * Initializes projection for this TileSource with current projection
324 */
325 protected void initProjection() {
326 initProjection(Main.getProjection());
327 }
328
329 /**
330 * Initializes projection for this TileSource with projection
331 * @param proj projection to be used by this TileSource
332 */
333 public void initProjection(Projection proj) {
334 this.currentTileMatrixSet = currentLayer.tileMatrixSetByCRS.get(proj.toCode());
335 if (this.currentTileMatrixSet == null) {
336 Main.warn("Unsupported CRS selected");
337 // take first, maybe it will work (if user sets custom projections, codes will not match)
338 this.currentTileMatrixSet = currentLayer.tileMatrixSetByCRS.values().iterator().next();
339 }
340 this.crsScale = getTileSize() * 0.28e-03 / proj.getMetersPerUnit();
341 }
342
343 @Override
344 public int getDefaultTileSize() {
345 return getTileSize();
346 }
347
348 // FIXME: remove in September 2015, when ImageryPreferenceEntry.tileSize will be initialized to -1 instead to 256
349 // need to leave it as it is to keep compatiblity between tested and latest JOSM versions
350 @Override
351 public int getTileSize() {
352 TileMatrix matrix = getTileMatrix(1);
353 if (matrix == null) {
354 return 1;
355 }
356 return matrix.tileHeight;
357 }
358
359 @Override
360 public String getTileUrl(int zoom, int tilex, int tiley) throws IOException {
361 String url;
362 switch (transferMode) {
363 case KVP:
364 url = baseUrl + URL_GET_ENCODING_PARAMS;
365 break;
366 case REST:
367 url = currentLayer.baseUrl;
368 break;
369 default:
370 url = "";
371 break;
372 }
373
374 TileMatrix tileMatrix = getTileMatrix(zoom);
375
376 if (tileMatrix == null) {
377 return ""; // no matrix, probably unsupported CRS selected.
378 }
379
380 return url.replaceAll("\\{layer\\}", this.currentLayer.name)
381 .replaceAll("\\{format\\}", this.currentLayer.format)
382 .replaceAll("\\{TileMatrixSet\\}", this.currentTileMatrixSet.identifier)
383 .replaceAll("\\{TileMatrix\\}", tileMatrix.identifier)
384 .replaceAll("\\{TileRow\\}", Integer.toString(tiley))
385 .replaceAll("\\{TileCol\\}", Integer.toString(tilex))
386 .replaceAll("\\{Style\\}", this.style);
387 }
388
389 /**
390 *
391 * @param zoom zoom level
392 * @return TileMatrix that's working on this zoom level
393 */
394 private TileMatrix getTileMatrix(int zoom) {
395 if (zoom > getMaxZoom()) {
396 return null;
397 }
398 if (zoom < 1) {
399 return null;
400 }
401 return this.currentTileMatrixSet.tileMatrix.toArray(new TileMatrix[]{})[zoom - 1];
402 }
403
404 @Override
405 public double getDistance(double lat1, double lon1, double lat2, double lon2) {
406 throw new UnsupportedOperationException("Not implemented");
407 }
408
409 @Override
410 public int lonToX(double lon, int zoom) {
411 throw new UnsupportedOperationException("Not implemented");
412 }
413
414 @Override
415 public int latToY(double lat, int zoom) {
416 throw new UnsupportedOperationException("Not implemented");
417 }
418
419 @Override
420 public double XToLon(int x, int zoom) {
421 throw new UnsupportedOperationException("Not implemented");
422 }
423
424 @Override
425 public double YToLat(int y, int zoom) {
426 throw new UnsupportedOperationException("Not implemented");
427 }
428
429 @Override
430 public double latToTileY(double lat, int zoom) {
431 throw new UnsupportedOperationException("Not implemented");
432 }
433
434 @Override
435 public ICoordinate tileXYToLatLon(Tile tile) {
436 return tileXYToLatLon(tile.getXtile(), tile.getYtile(), tile.getZoom());
437 }
438
439 @Override
440 public ICoordinate tileXYToLatLon(TileXY xy, int zoom) {
441 return tileXYToLatLon(xy.getXIndex(), xy.getYIndex(), zoom);
442 }
443
444 @Override
445 public ICoordinate tileXYToLatLon(int x, int y, int zoom) {
446 TileMatrix matrix = getTileMatrix(zoom);
447 if (matrix == null) {
448 return Main.getProjection().getWorldBoundsLatLon().getCenter().toCoordinate();
449 }
450 double scale = matrix.scaleDenominator * this.crsScale;
451 EastNorth ret = new EastNorth(matrix.topLeftCorner.east() + x * scale, matrix.topLeftCorner.north() - y * scale);
452 return Main.getProjection().eastNorth2latlon(ret).toCoordinate();
453 }
454
455 @Override
456 public TileXY latLonToTileXY(double lat, double lon, int zoom) {
457 Projection proj = Main.getProjection();
458 EastNorth enPoint = proj.latlon2eastNorth(new LatLon(lat, lon));
459 TileMatrix matrix = getTileMatrix(zoom);
460 if (matrix == null) {
461 return new TileXY(0, 0);
462 }
463 double scale = matrix.scaleDenominator * this.crsScale;
464 return new TileXY(
465 (enPoint.east() - matrix.topLeftCorner.east()) / scale,
466 (matrix.topLeftCorner.north() - enPoint.north()) / scale
467 );
468 }
469
470 @Override
471 public TileXY latLonToTileXY(ICoordinate point, int zoom) {
472 return latLonToTileXY(point.getLat(), point.getLon(), zoom);
473 }
474
475 @Override
476 public int getTileXMax(int zoom) {
477 return getTileXMax(zoom, Main.getProjection());
478 }
479
480 @Override
481 public int getTileXMin(int zoom) {
482 return 0;
483 }
484
485 @Override
486 public int getTileYMax(int zoom) {
487 return getTileYMax(zoom, Main.getProjection());
488 }
489
490 @Override
491 public int getTileYMin(int zoom) {
492 return 0;
493 }
494
495 @Override
496 public Point latLonToXY(double lat, double lon, int zoom) {
497 TileMatrix matrix = getTileMatrix(zoom);
498 if (matrix == null) {
499 return new Point(0, 0);
500 }
501 double scale = matrix.scaleDenominator * this.crsScale;
502 EastNorth point = Main.getProjection().latlon2eastNorth(new LatLon(lat, lon));
503 return new Point(
504 (int) Math.round((point.east() - matrix.topLeftCorner.east()) / scale),
505 (int) Math.round((matrix.topLeftCorner.north() - point.north()) / scale)
506 );
507 }
508
509 @Override
510 public Point latLonToXY(ICoordinate point, int zoom) {
511 return latLonToXY(point.getLat(), point.getLon(), zoom);
512 }
513
514 @Override
515 public Coordinate XYToLatLon(Point point, int zoom) {
516 return XYToLatLon(point.x, point.y, zoom);
517 }
518
519 @Override
520 public Coordinate XYToLatLon(int x, int y, int zoom) {
521 TileMatrix matrix = getTileMatrix(zoom);
522 if (matrix == null) {
523 return new Coordinate(0, 0);
524 }
525 double scale = matrix.scaleDenominator * this.crsScale;
526 Projection proj = Main.getProjection();
527 EastNorth ret = new EastNorth(
528 matrix.topLeftCorner.east() + x * scale,
529 matrix.topLeftCorner.north() - y * scale
530 );
531 LatLon ll = proj.eastNorth2latlon(ret);
532 return new Coordinate(ll.lat(), ll.lon());
533 }
534
535 @Override
536 public double lonToTileX(double lon, int zoom) {
537 throw new UnsupportedOperationException("Not implemented");
538 }
539
540 @Override
541 public double tileXToLon(int x, int zoom) {
542 throw new UnsupportedOperationException("Not implemented");
543 }
544
545 @Override
546 public double tileYToLat(int y, int zoom) {
547 throw new UnsupportedOperationException("Not implemented");
548 }
549
550 @Override
551 public Map<String, String> getHeaders() {
552 return headers;
553 }
554
555 @Override
556 public int getMaxZoom() {
557 if (this.currentTileMatrixSet != null) {
558 return this.currentTileMatrixSet.tileMatrix.size();
559 }
560 return 0;
561 }
562
563 /**
564 * Checks if url is acceptable by this Tile Source
565 * @param url URL to check
566 */
567 public static void checkUrl(String url) {
568 CheckParameterUtil.ensureParameterNotNull(url, "url");
569 Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);
570 while (m.find()) {
571 boolean isSupportedPattern = false;
572 for (String pattern : ALL_PATTERNS) {
573 if (m.group().matches(pattern)) {
574 isSupportedPattern = true;
575 break;
576 }
577 }
578 if (!isSupportedPattern) {
579 throw new IllegalArgumentException(
580 tr("{0} is not a valid WMS argument. Please check this server URL:\n{1}", m.group(), url));
581 }
582 }
583 }
584
585 /**
586 * @return set of projection codes that this TileSource supports
587 */
588 public Set<String> getSupportedProjections() {
589 return this.currentLayer.tileMatrixSetByCRS.keySet();
590 }
591
592 private int getTileYMax(int zoom, Projection proj) {
593 TileMatrix matrix = getTileMatrix(zoom);
594 if (matrix == null) {
595 return 0;
596 }
597 double scale = matrix.scaleDenominator * this.crsScale;
598 Bounds bounds = Main.getProjection().getWorldBoundsLatLon();
599 EastNorth min = proj.latlon2eastNorth(bounds.getMin());
600 EastNorth max = proj.latlon2eastNorth(bounds.getMax());
601 return (int) Math.ceil(Math.abs(max.north() - min.north()) / scale);
602 }
603
604 private int getTileXMax(int zoom, Projection proj) {
605 TileMatrix matrix = getTileMatrix(zoom);
606 if (matrix == null) {
607 return 0;
608 }
609 double scale = matrix.scaleDenominator * this.crsScale;
610 Bounds bounds = Main.getProjection().getWorldBoundsLatLon();
611 EastNorth min = proj.latlon2eastNorth(bounds.getMin());
612 EastNorth max = proj.latlon2eastNorth(bounds.getMax());
613 return (int) Math.ceil(Math.abs(max.east() - min.east()) / scale);
614 }
615}
Note: See TracBrowser for help on using the repository browser.