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

Last change on this file since 13792 was 13759, checked in by Don-vip, 6 years ago

typo

  • Property svn:eol-style set to native
File size: 45.4 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.io.ByteArrayInputStream;
8import java.io.IOException;
9import java.io.InputStream;
10import java.nio.charset.StandardCharsets;
11import java.nio.file.InvalidPathException;
12import java.util.ArrayList;
13import java.util.Arrays;
14import java.util.Collection;
15import java.util.Collections;
16import java.util.LinkedHashSet;
17import java.util.List;
18import java.util.Map;
19import java.util.Map.Entry;
20import java.util.Objects;
21import java.util.Optional;
22import java.util.SortedSet;
23import java.util.Stack;
24import java.util.TreeSet;
25import java.util.concurrent.ConcurrentHashMap;
26import java.util.regex.Matcher;
27import java.util.regex.Pattern;
28import java.util.stream.Collectors;
29
30import javax.imageio.ImageIO;
31import javax.xml.namespace.QName;
32import javax.xml.stream.XMLStreamException;
33import javax.xml.stream.XMLStreamReader;
34
35import org.openstreetmap.gui.jmapviewer.Coordinate;
36import org.openstreetmap.gui.jmapviewer.Projected;
37import org.openstreetmap.gui.jmapviewer.Tile;
38import org.openstreetmap.gui.jmapviewer.TileRange;
39import org.openstreetmap.gui.jmapviewer.TileXY;
40import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
41import org.openstreetmap.gui.jmapviewer.interfaces.IProjected;
42import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
43import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
44import org.openstreetmap.josm.Main;
45import org.openstreetmap.josm.data.ProjectionBounds;
46import org.openstreetmap.josm.data.coor.EastNorth;
47import org.openstreetmap.josm.data.coor.LatLon;
48import org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.TransferMode;
49import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
50import org.openstreetmap.josm.data.projection.Projection;
51import org.openstreetmap.josm.data.projection.Projections;
52import org.openstreetmap.josm.gui.ExtendedDialog;
53import org.openstreetmap.josm.gui.layer.NativeScaleLayer.ScaleList;
54import org.openstreetmap.josm.gui.layer.imagery.WMTSLayerSelection;
55import org.openstreetmap.josm.io.CachedFile;
56import org.openstreetmap.josm.spi.preferences.Config;
57import org.openstreetmap.josm.tools.CheckParameterUtil;
58import org.openstreetmap.josm.tools.Logging;
59import org.openstreetmap.josm.tools.Utils;
60
61/**
62 * Tile Source handling WMTS providers
63 *
64 * @author Wiktor Niesiobędzki
65 * @since 8526
66 */
67public class WMTSTileSource extends AbstractTMSTileSource implements TemplatedTileSource {
68 /**
69 * WMTS namespace address
70 */
71 public static final String WMTS_NS_URL = "http://www.opengis.net/wmts/1.0";
72
73 // CHECKSTYLE.OFF: SingleSpaceSeparator
74 private static final QName QN_CONTENTS = new QName(WMTSTileSource.WMTS_NS_URL, "Contents");
75 private static final QName QN_DEFAULT = new QName(WMTSTileSource.WMTS_NS_URL, "Default");
76 private static final QName QN_DIMENSION = new QName(WMTSTileSource.WMTS_NS_URL, "Dimension");
77 private static final QName QN_FORMAT = new QName(WMTSTileSource.WMTS_NS_URL, "Format");
78 private static final QName QN_LAYER = new QName(WMTSTileSource.WMTS_NS_URL, "Layer");
79 private static final QName QN_MATRIX_WIDTH = new QName(WMTSTileSource.WMTS_NS_URL, "MatrixWidth");
80 private static final QName QN_MATRIX_HEIGHT = new QName(WMTSTileSource.WMTS_NS_URL, "MatrixHeight");
81 private static final QName QN_RESOURCE_URL = new QName(WMTSTileSource.WMTS_NS_URL, "ResourceURL");
82 private static final QName QN_SCALE_DENOMINATOR = new QName(WMTSTileSource.WMTS_NS_URL, "ScaleDenominator");
83 private static final QName QN_STYLE = new QName(WMTSTileSource.WMTS_NS_URL, "Style");
84 private static final QName QN_TILEMATRIX = new QName(WMTSTileSource.WMTS_NS_URL, "TileMatrix");
85 private static final QName QN_TILEMATRIXSET = new QName(WMTSTileSource.WMTS_NS_URL, "TileMatrixSet");
86 private static final QName QN_TILEMATRIX_SET_LINK = new QName(WMTSTileSource.WMTS_NS_URL, "TileMatrixSetLink");
87 private static final QName QN_TILE_WIDTH = new QName(WMTSTileSource.WMTS_NS_URL, "TileWidth");
88 private static final QName QN_TILE_HEIGHT = new QName(WMTSTileSource.WMTS_NS_URL, "TileHeight");
89 private static final QName QN_TOPLEFT_CORNER = new QName(WMTSTileSource.WMTS_NS_URL, "TopLeftCorner");
90 private static final QName QN_VALUE = new QName(WMTSTileSource.WMTS_NS_URL, "Value");
91 // CHECKSTYLE.ON: SingleSpaceSeparator
92
93 private static final String PATTERN_HEADER = "\\{header\\(([^,]+),([^}]+)\\)\\}";
94
95 private static final String URL_GET_ENCODING_PARAMS = "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER={layer}&STYLE={style}&"
96 + "FORMAT={format}&tileMatrixSet={TileMatrixSet}&tileMatrix={TileMatrix}&tileRow={TileRow}&tileCol={TileCol}";
97
98 private static final String[] ALL_PATTERNS = {
99 PATTERN_HEADER,
100 };
101
102 private int cachedTileSize = -1;
103
104 private static class TileMatrix {
105 private String identifier;
106 private double scaleDenominator;
107 private EastNorth topLeftCorner;
108 private int tileWidth;
109 private int tileHeight;
110 private int matrixWidth = -1;
111 private int matrixHeight = -1;
112 }
113
114 private static class TileMatrixSetBuilder {
115 // sorted by zoom level
116 SortedSet<TileMatrix> tileMatrix = new TreeSet<>((o1, o2) -> -1 * Double.compare(o1.scaleDenominator, o2.scaleDenominator));
117 private String crs;
118 private String identifier;
119
120 TileMatrixSet build() {
121 return new TileMatrixSet(this);
122 }
123 }
124
125 /**
126 *
127 * class representing WMTS TileMatrixSet
128 * This connects projection and TileMatrix (how the map is divided in tiles)
129 *
130 */
131 public static class TileMatrixSet {
132
133 private final List<TileMatrix> tileMatrix;
134 private final String crs;
135 private final String identifier;
136
137 TileMatrixSet(TileMatrixSet tileMatrixSet) {
138 if (tileMatrixSet != null) {
139 tileMatrix = new ArrayList<>(tileMatrixSet.tileMatrix);
140 crs = tileMatrixSet.crs;
141 identifier = tileMatrixSet.identifier;
142 } else {
143 tileMatrix = Collections.emptyList();
144 crs = null;
145 identifier = null;
146 }
147 }
148
149 TileMatrixSet(TileMatrixSetBuilder builder) {
150 tileMatrix = new ArrayList<>(builder.tileMatrix);
151 crs = builder.crs;
152 identifier = builder.identifier;
153 }
154
155 @Override
156 public String toString() {
157 return "TileMatrixSet [crs=" + crs + ", identifier=" + identifier + ']';
158 }
159
160 /**
161 *
162 * @return identifier of this TileMatrixSet
163 */
164 public String getIdentifier() {
165 return identifier;
166 }
167
168 /**
169 *
170 * @return projection of this tileMatrix
171 */
172 public String getCrs() {
173 return crs;
174 }
175 }
176
177 private static class Dimension {
178 private String identifier;
179 private String defaultValue;
180 private final List<String> values = new ArrayList<>();
181 }
182
183 /**
184 * Class representing WMTS Layer information
185 *
186 */
187 public static class Layer {
188 private String format;
189 private String identifier;
190 private String title;
191 private TileMatrixSet tileMatrixSet;
192 private String baseUrl;
193 private String style;
194 private final Collection<String> tileMatrixSetLinks = new ArrayList<>();
195 private final Collection<Dimension> dimensions = new ArrayList<>();
196
197 Layer(Layer l) {
198 Objects.requireNonNull(l);
199 format = l.format;
200 identifier = l.identifier;
201 title = l.title;
202 baseUrl = l.baseUrl;
203 style = l.style;
204 tileMatrixSet = new TileMatrixSet(l.tileMatrixSet);
205 dimensions.addAll(l.dimensions);
206 }
207
208 Layer() {
209 }
210
211 /**
212 * Get title of the layer for user display.
213 *
214 * This is either the content of the Title element (if available) or
215 * the layer identifier (as fallback)
216 * @return title of the layer for user display
217 */
218 public String getUserTitle() {
219 return title != null ? title : identifier;
220 }
221
222 @Override
223 public String toString() {
224 return "Layer [identifier=" + identifier + ", title=" + title + ", tileMatrixSet="
225 + tileMatrixSet + ", baseUrl=" + baseUrl + ", style=" + style + ']';
226 }
227
228 /**
229 *
230 * @return identifier of this layer
231 */
232 public String getIdentifier() {
233 return identifier;
234 }
235
236 /**
237 *
238 * @return style of this layer
239 */
240 public String getStyle() {
241 return style;
242 }
243
244 /**
245 *
246 * @return tileMatrixSet of this layer
247 */
248 public TileMatrixSet getTileMatrixSet() {
249 return tileMatrixSet;
250 }
251 }
252
253 /**
254 * Exception thrown when praser doesn't find expected information in GetCapabilities document
255 *
256 */
257 public static class WMTSGetCapabilitiesException extends Exception {
258
259 /**
260 * @param cause description of cause
261 */
262 public WMTSGetCapabilitiesException(String cause) {
263 super(cause);
264 }
265
266 /**
267 * @param cause description of cause
268 * @param t nested exception
269 */
270 public WMTSGetCapabilitiesException(String cause, Throwable t) {
271 super(cause, t);
272 }
273 }
274
275 private static final class SelectLayerDialog extends ExtendedDialog {
276 private final transient List<Entry<String, List<Layer>>> layers;
277 private final WMTSLayerSelection list;
278
279 SelectLayerDialog(Collection<Layer> layers) {
280 super(Main.parent, tr("Select WMTS layer"), tr("Add layers"), tr("Cancel"));
281 this.layers = groupLayersByNameAndTileMatrixSet(layers);
282 this.list = new WMTSLayerSelection(this.layers);
283 setContent(list);
284 }
285
286 public DefaultLayer getSelectedLayer() {
287 Layer selectedLayer = list.getSelectedLayer();
288 return new DefaultLayer(ImageryType.WMTS, selectedLayer.identifier, selectedLayer.style, selectedLayer.tileMatrixSet.identifier);
289 }
290
291 }
292
293 private final Map<String, String> headers = new ConcurrentHashMap<>();
294 private final Collection<Layer> layers;
295 private Layer currentLayer;
296 private TileMatrixSet currentTileMatrixSet;
297 private double crsScale;
298 private GetCapabilitiesParseHelper.TransferMode transferMode;
299
300 private ScaleList nativeScaleList;
301
302 private final DefaultLayer defaultLayer;
303
304 private Projection tileProjection;
305
306 /**
307 * Creates a tile source based on imagery info
308 * @param info imagery info
309 * @throws IOException if any I/O error occurs
310 * @throws WMTSGetCapabilitiesException when document didn't contain any layers
311 * @throws IllegalArgumentException if any other error happens for the given imagery info
312 */
313 public WMTSTileSource(ImageryInfo info) throws IOException, WMTSGetCapabilitiesException {
314 super(info);
315 CheckParameterUtil.ensureThat(info.getDefaultLayers().size() < 2, "At most 1 default layer for WMTS is supported");
316 this.headers.putAll(info.getCustomHttpHeaders());
317 this.baseUrl = GetCapabilitiesParseHelper.normalizeCapabilitiesUrl(handleTemplate(info.getUrl()));
318 WMTSCapabilities capabilities = getCapabilities(baseUrl, headers);
319 this.layers = capabilities.getLayers();
320 this.baseUrl = capabilities.getBaseUrl();
321 this.transferMode = capabilities.getTransferMode();
322 if (info.getDefaultLayers().isEmpty()) {
323 Logging.warn(tr("No default layer selected, choosing first layer."));
324 if (!layers.isEmpty()) {
325 Layer first = layers.iterator().next();
326 this.defaultLayer = new DefaultLayer(info.getImageryType(), first.identifier, first.style, first.tileMatrixSet.identifier);
327 } else {
328 this.defaultLayer = null;
329 }
330 } else {
331 this.defaultLayer = info.getDefaultLayers().iterator().next();
332 }
333 if (this.layers.isEmpty())
334 throw new IllegalArgumentException(tr("No layers defined by getCapabilities document: {0}", info.getUrl()));
335 }
336
337 /**
338 * Creates a dialog based on this tile source with all available layers and returns the name of selected layer
339 * @return Name of selected layer
340 */
341 public DefaultLayer userSelectLayer() {
342 Map<String, List<Layer>> layerById = layers.stream().collect(
343 Collectors.groupingBy(x -> x.identifier));
344 if (layerById.size() == 1) { // only one layer
345 List<Layer> ls = layerById.entrySet().iterator().next().getValue()
346 .stream().filter(
347 u -> u.tileMatrixSet.crs.equals(Main.getProjection().toCode()))
348 .collect(Collectors.toList());
349 if (ls.size() == 1) {
350 // only one tile matrix set with matching projection - no point in asking
351 Layer selectedLayer = ls.get(0);
352 return new DefaultLayer(ImageryType.WMTS, selectedLayer.identifier, selectedLayer.style, selectedLayer.tileMatrixSet.identifier);
353 }
354 }
355
356 final SelectLayerDialog layerSelection = new SelectLayerDialog(layers);
357 if (layerSelection.showDialog().getValue() == 1) {
358 return layerSelection.getSelectedLayer();
359 }
360 return null;
361 }
362
363 private String handleTemplate(String url) {
364 Pattern pattern = Pattern.compile(PATTERN_HEADER);
365 StringBuffer output = new StringBuffer();
366 Matcher matcher = pattern.matcher(url);
367 while (matcher.find()) {
368 this.headers.put(matcher.group(1), matcher.group(2));
369 matcher.appendReplacement(output, "");
370 }
371 matcher.appendTail(output);
372 return output.toString();
373 }
374
375
376 /**
377 * @param url of the getCapabilities document
378 * @param headers HTTP headers to set when calling getCapabilities url
379 * @return capabilities
380 * @throws IOException in case of any I/O error
381 * @throws WMTSGetCapabilitiesException when document didn't contain any layers
382 * @throws IllegalArgumentException in case of any other error
383 */
384 public static WMTSCapabilities getCapabilities(String url, Map<String, String> headers) throws IOException, WMTSGetCapabilitiesException {
385 try (CachedFile cf = new CachedFile(url); InputStream in = cf.setHttpHeaders(headers).
386 setMaxAge(Config.getPref().getLong("wmts.capabilities.cache.max_age", 7 * CachedFile.DAYS)).
387 setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince).
388 getInputStream()) {
389 byte[] data = Utils.readBytesFromStream(in);
390 if (data.length == 0) {
391 cf.clear();
392 throw new IllegalArgumentException("Could not read data from: " + url);
393 }
394
395 try {
396 XMLStreamReader reader = GetCapabilitiesParseHelper.getReader(new ByteArrayInputStream(data));
397 WMTSCapabilities ret = null;
398 Collection<Layer> layers = null;
399 for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) {
400 if (event == XMLStreamReader.START_ELEMENT) {
401 if (GetCapabilitiesParseHelper.QN_OWS_OPERATIONS_METADATA.equals(reader.getName())) {
402 ret = parseOperationMetadata(reader);
403 }
404
405 if (QN_CONTENTS.equals(reader.getName())) {
406 layers = parseContents(reader);
407 }
408 }
409 }
410 if (ret == null) {
411 /*
412 * see #12168 - create dummy operation metadata - not all WMTS services provide this information
413 *
414 * WMTS Standard:
415 * > Resource oriented architecture style HTTP encodings SHALL not be described in the OperationsMetadata section.
416 *
417 * And OperationMetada is not mandatory element. So REST mode is justifiable
418 */
419 ret = new WMTSCapabilities(url, TransferMode.REST);
420 }
421 if (layers == null) {
422 throw new WMTSGetCapabilitiesException(tr("WMTS Capabilities document did not contain layers in url: {0}", url));
423 }
424 ret.addLayers(layers);
425 return ret;
426 } catch (XMLStreamException e) {
427 cf.clear();
428 Logging.warn(new String(data, StandardCharsets.UTF_8));
429 throw new WMTSGetCapabilitiesException(tr("Error during parsing of WMTS Capabilities document: {0}", e.getMessage()), e);
430 }
431 } catch (InvalidPathException e) {
432 throw new WMTSGetCapabilitiesException(tr("Invalid path for GetCapabilities document: {0}", e.getMessage()), e);
433 }
434 }
435
436 /**
437 * Parse Contents tag. Returns when reader reaches Contents closing tag
438 *
439 * @param reader StAX reader instance
440 * @return collection of layers within contents with properly linked TileMatrixSets
441 * @throws XMLStreamException See {@link XMLStreamReader}
442 */
443 private static Collection<Layer> parseContents(XMLStreamReader reader) throws XMLStreamException {
444 Map<String, TileMatrixSet> matrixSetById = new ConcurrentHashMap<>();
445 Collection<Layer> layers = new ArrayList<>();
446 for (int event = reader.getEventType();
447 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_CONTENTS.equals(reader.getName()));
448 event = reader.next()) {
449 if (event == XMLStreamReader.START_ELEMENT) {
450 if (QN_LAYER.equals(reader.getName())) {
451 Layer l = parseLayer(reader);
452 if (l != null) {
453 layers.add(l);
454 }
455 }
456 if (QN_TILEMATRIXSET.equals(reader.getName())) {
457 TileMatrixSet entry = parseTileMatrixSet(reader);
458 matrixSetById.put(entry.identifier, entry);
459 }
460 }
461 }
462 Collection<Layer> ret = new ArrayList<>();
463 // link layers to matrix sets
464 for (Layer l: layers) {
465 for (String tileMatrixId: l.tileMatrixSetLinks) {
466 Layer newLayer = new Layer(l); // create a new layer object for each tile matrix set supported
467 newLayer.tileMatrixSet = matrixSetById.get(tileMatrixId);
468 ret.add(newLayer);
469 }
470 }
471 return ret;
472 }
473
474 /**
475 * Parse Layer tag. Returns when reader will reach Layer closing tag
476 *
477 * @param reader StAX reader instance
478 * @return Layer object, with tileMatrixSetLinks and no tileMatrixSet attribute set.
479 * @throws XMLStreamException See {@link XMLStreamReader}
480 */
481 private static Layer parseLayer(XMLStreamReader reader) throws XMLStreamException {
482 Layer layer = new Layer();
483 Stack<QName> tagStack = new Stack<>();
484 List<String> supportedMimeTypes = new ArrayList<>(Arrays.asList(ImageIO.getReaderMIMETypes()));
485 supportedMimeTypes.add("image/jpgpng"); // used by ESRI
486 supportedMimeTypes.add("image/png8"); // used by geoserver
487 if (supportedMimeTypes.contains("image/jpeg")) {
488 supportedMimeTypes.add("image/jpg"); // sometimes mispelled by Arcgis
489 }
490 Collection<String> unsupportedFormats = new ArrayList<>();
491
492 for (int event = reader.getEventType();
493 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_LAYER.equals(reader.getName()));
494 event = reader.next()) {
495 if (event == XMLStreamReader.START_ELEMENT) {
496 tagStack.push(reader.getName());
497 if (tagStack.size() == 2) {
498 if (QN_FORMAT.equals(reader.getName())) {
499 String format = reader.getElementText();
500 if (supportedMimeTypes.contains(format)) {
501 layer.format = format;
502 } else {
503 unsupportedFormats.add(format);
504 }
505 } else if (GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER.equals(reader.getName())) {
506 layer.identifier = reader.getElementText();
507 } else if (GetCapabilitiesParseHelper.QN_OWS_TITLE.equals(reader.getName())) {
508 layer.title = reader.getElementText();
509 } else if (QN_RESOURCE_URL.equals(reader.getName()) &&
510 "tile".equals(reader.getAttributeValue("", "resourceType"))) {
511 layer.baseUrl = reader.getAttributeValue("", "template");
512 } else if (QN_STYLE.equals(reader.getName()) &&
513 "true".equals(reader.getAttributeValue("", "isDefault"))) {
514 if (GetCapabilitiesParseHelper.moveReaderToTag(reader, GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER)) {
515 layer.style = reader.getElementText();
516 tagStack.push(reader.getName()); // keep tagStack in sync
517 }
518 } else if (QN_DIMENSION.equals(reader.getName())) {
519 layer.dimensions.add(parseDimension(reader));
520 } else if (QN_TILEMATRIX_SET_LINK.equals(reader.getName())) {
521 layer.tileMatrixSetLinks.add(parseTileMatrixSetLink(reader));
522 } else {
523 GetCapabilitiesParseHelper.moveReaderToEndCurrentTag(reader);
524 }
525 }
526 }
527 // need to get event type from reader, as parsing might have change position of reader
528 if (reader.getEventType() == XMLStreamReader.END_ELEMENT) {
529 QName start = tagStack.pop();
530 if (!start.equals(reader.getName())) {
531 throw new IllegalStateException(tr("WMTS Parser error - start element {0} has different name than end element {2}",
532 start, reader.getName()));
533 }
534 }
535 }
536 if (layer.style == null) {
537 layer.style = "";
538 }
539 if (layer.format == null) {
540 // no format found - it's mandatory parameter - can't use this layer
541 Logging.warn(tr("Can''t use layer {0} because no supported formats where found. Layer is available in formats: {1}",
542 layer.getUserTitle(),
543 String.join(", ", unsupportedFormats)));
544 return null;
545 }
546 return layer;
547 }
548
549 /**
550 * Gets Dimension value. Returns when reader is on Dimension closing tag
551 *
552 * @param reader StAX reader instance
553 * @return dimension
554 * @throws XMLStreamException See {@link XMLStreamReader}
555 */
556 private static Dimension parseDimension(XMLStreamReader reader) throws XMLStreamException {
557 Dimension ret = new Dimension();
558 for (int event = reader.getEventType();
559 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT &&
560 QN_DIMENSION.equals(reader.getName()));
561 event = reader.next()) {
562 if (event == XMLStreamReader.START_ELEMENT) {
563 if (GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER.equals(reader.getName())) {
564 ret.identifier = reader.getElementText();
565 } else if (QN_DEFAULT.equals(reader.getName())) {
566 ret.defaultValue = reader.getElementText();
567 } else if (QN_VALUE.equals(reader.getName())) {
568 ret.values.add(reader.getElementText());
569 }
570 }
571 }
572 return ret;
573 }
574
575 /**
576 * Gets TileMatrixSetLink value. Returns when reader is on TileMatrixSetLink closing tag
577 *
578 * @param reader StAX reader instance
579 * @return TileMatrixSetLink identifier
580 * @throws XMLStreamException See {@link XMLStreamReader}
581 */
582 private static String parseTileMatrixSetLink(XMLStreamReader reader) throws XMLStreamException {
583 String ret = null;
584 for (int event = reader.getEventType();
585 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT &&
586 QN_TILEMATRIX_SET_LINK.equals(reader.getName()));
587 event = reader.next()) {
588 if (event == XMLStreamReader.START_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName())) {
589 ret = reader.getElementText();
590 }
591 }
592 return ret;
593 }
594
595 /**
596 * Parses TileMatrixSet section. Returns when reader is on TileMatrixSet closing tag
597 * @param reader StAX reader instance
598 * @return TileMatrixSet object
599 * @throws XMLStreamException See {@link XMLStreamReader}
600 */
601 private static TileMatrixSet parseTileMatrixSet(XMLStreamReader reader) throws XMLStreamException {
602 TileMatrixSetBuilder matrixSet = new TileMatrixSetBuilder();
603 for (int event = reader.getEventType();
604 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName()));
605 event = reader.next()) {
606 if (event == XMLStreamReader.START_ELEMENT) {
607 if (GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER.equals(reader.getName())) {
608 matrixSet.identifier = reader.getElementText();
609 }
610 if (GetCapabilitiesParseHelper.QN_OWS_SUPPORTED_CRS.equals(reader.getName())) {
611 matrixSet.crs = GetCapabilitiesParseHelper.crsToCode(reader.getElementText());
612 }
613 if (QN_TILEMATRIX.equals(reader.getName())) {
614 matrixSet.tileMatrix.add(parseTileMatrix(reader, matrixSet.crs));
615 }
616 }
617 }
618 return matrixSet.build();
619 }
620
621 /**
622 * Parses TileMatrix section. Returns when reader is on TileMatrix closing tag.
623 * @param reader StAX reader instance
624 * @param matrixCrs projection used by this matrix
625 * @return TileMatrix object
626 * @throws XMLStreamException See {@link XMLStreamReader}
627 */
628 private static TileMatrix parseTileMatrix(XMLStreamReader reader, String matrixCrs) throws XMLStreamException {
629 Projection matrixProj = Optional.ofNullable(Projections.getProjectionByCode(matrixCrs))
630 .orElseGet(Main::getProjection); // use current projection if none found. Maybe user is using custom string
631 TileMatrix ret = new TileMatrix();
632 for (int event = reader.getEventType();
633 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_TILEMATRIX.equals(reader.getName()));
634 event = reader.next()) {
635 if (event == XMLStreamReader.START_ELEMENT) {
636 if (GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER.equals(reader.getName())) {
637 ret.identifier = reader.getElementText();
638 }
639 if (QN_SCALE_DENOMINATOR.equals(reader.getName())) {
640 ret.scaleDenominator = Double.parseDouble(reader.getElementText());
641 }
642 if (QN_TOPLEFT_CORNER.equals(reader.getName())) {
643 String[] topLeftCorner = reader.getElementText().split(" ");
644 if (matrixProj.switchXY()) {
645 ret.topLeftCorner = new EastNorth(Double.parseDouble(topLeftCorner[1]), Double.parseDouble(topLeftCorner[0]));
646 } else {
647 ret.topLeftCorner = new EastNorth(Double.parseDouble(topLeftCorner[0]), Double.parseDouble(topLeftCorner[1]));
648 }
649 }
650 if (QN_TILE_HEIGHT.equals(reader.getName())) {
651 ret.tileHeight = Integer.parseInt(reader.getElementText());
652 }
653 if (QN_TILE_WIDTH.equals(reader.getName())) {
654 ret.tileWidth = Integer.parseInt(reader.getElementText());
655 }
656 if (QN_MATRIX_HEIGHT.equals(reader.getName())) {
657 ret.matrixHeight = Integer.parseInt(reader.getElementText());
658 }
659 if (QN_MATRIX_WIDTH.equals(reader.getName())) {
660 ret.matrixWidth = Integer.parseInt(reader.getElementText());
661 }
662 }
663 }
664 if (ret.tileHeight != ret.tileWidth) {
665 throw new AssertionError(tr("Only square tiles are supported. {0}x{1} returned by server for TileMatrix identifier {2}",
666 ret.tileHeight, ret.tileWidth, ret.identifier));
667 }
668 return ret;
669 }
670
671 /**
672 * Parses OperationMetadata section. Returns when reader is on OperationsMetadata closing tag.
673 * return WMTSCapabilities with baseUrl and transferMode
674 *
675 * @param reader StAX reader instance
676 * @return WMTSCapabilities with baseUrl and transferMode set
677 * @throws XMLStreamException See {@link XMLStreamReader}
678 */
679 private static WMTSCapabilities parseOperationMetadata(XMLStreamReader reader) throws XMLStreamException {
680 for (int event = reader.getEventType();
681 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT &&
682 GetCapabilitiesParseHelper.QN_OWS_OPERATIONS_METADATA.equals(reader.getName()));
683 event = reader.next()) {
684 if (event == XMLStreamReader.START_ELEMENT &&
685 GetCapabilitiesParseHelper.QN_OWS_OPERATION.equals(reader.getName()) &&
686 "GetTile".equals(reader.getAttributeValue("", "name")) &&
687 GetCapabilitiesParseHelper.moveReaderToTag(reader,
688 GetCapabilitiesParseHelper.QN_OWS_DCP,
689 GetCapabilitiesParseHelper.QN_OWS_HTTP,
690 GetCapabilitiesParseHelper.QN_OWS_GET
691 )) {
692 return new WMTSCapabilities(
693 reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href"),
694 GetCapabilitiesParseHelper.getTransferMode(reader)
695 );
696 }
697 }
698 return null;
699 }
700
701 /**
702 * Initializes projection for this TileSource with projection
703 * @param proj projection to be used by this TileSource
704 */
705 public void initProjection(Projection proj) {
706 if (proj.equals(tileProjection))
707 return;
708 List<Layer> matchingLayers = layers.stream().filter(
709 l -> l.identifier.equals(defaultLayer.getLayerName()) && l.tileMatrixSet.crs.equals(proj.toCode()))
710 .collect(Collectors.toList());
711 if (matchingLayers.size() > 1) {
712 this.currentLayer = matchingLayers.stream().filter(
713 l -> l.tileMatrixSet.identifier.equals(defaultLayer.getTileMatrixSet()))
714 .findFirst().orElse(matchingLayers.get(0));
715 this.tileProjection = proj;
716 } else if (matchingLayers.size() == 1) {
717 this.currentLayer = matchingLayers.get(0);
718 this.tileProjection = proj;
719 } else {
720 // no tile matrix sets with current projection
721 if (this.currentLayer == null) {
722 this.tileProjection = null;
723 for (Layer layer : layers) {
724 if (!layer.identifier.equals(defaultLayer.getLayerName())) {
725 continue;
726 }
727 Projection pr = Projections.getProjectionByCode(layer.tileMatrixSet.crs);
728 if (pr != null) {
729 this.currentLayer = layer;
730 this.tileProjection = pr;
731 break;
732 }
733 }
734 if (this.currentLayer == null)
735 throw new IllegalArgumentException(
736 layers.stream().map(l -> l.tileMatrixSet).collect(Collectors.toList()).toString());
737 } // else: keep currentLayer and tileProjection as is
738 }
739 if (this.currentLayer != null) {
740 this.currentTileMatrixSet = this.currentLayer.tileMatrixSet;
741 Collection<Double> scales = new ArrayList<>(currentTileMatrixSet.tileMatrix.size());
742 for (TileMatrix tileMatrix : currentTileMatrixSet.tileMatrix) {
743 scales.add(tileMatrix.scaleDenominator * 0.28e-03);
744 }
745 this.nativeScaleList = new ScaleList(scales);
746 }
747 this.crsScale = getTileSize() * 0.28e-03 / this.tileProjection.getMetersPerUnit();
748 }
749
750 @Override
751 public int getTileSize() {
752 if (cachedTileSize > 0) {
753 return cachedTileSize;
754 }
755 if (currentTileMatrixSet != null) {
756 // no support for non-square tiles (tileHeight != tileWidth)
757 // and for different tile sizes at different zoom levels
758 cachedTileSize = currentTileMatrixSet.tileMatrix.get(0).tileHeight;
759 return cachedTileSize;
760 }
761 // Fallback to default mercator tile size. Maybe it will work
762 Logging.warn("WMTS: Could not determine tile size. Using default tile size of: {0}", getDefaultTileSize());
763 return getDefaultTileSize();
764 }
765
766 @Override
767 public String getTileUrl(int zoom, int tilex, int tiley) {
768 if (currentLayer == null) {
769 return "";
770 }
771
772 String url;
773 if (currentLayer.baseUrl != null && transferMode == null) {
774 url = currentLayer.baseUrl;
775 } else {
776 switch (transferMode) {
777 case KVP:
778 url = baseUrl + URL_GET_ENCODING_PARAMS;
779 break;
780 case REST:
781 url = currentLayer.baseUrl;
782 break;
783 default:
784 url = "";
785 break;
786 }
787 }
788
789 TileMatrix tileMatrix = getTileMatrix(zoom);
790
791 if (tileMatrix == null) {
792 return ""; // no matrix, probably unsupported CRS selected.
793 }
794
795 url = url.replaceAll("\\{layer\\}", this.currentLayer.identifier)
796 .replaceAll("\\{format\\}", this.currentLayer.format)
797 .replaceAll("\\{TileMatrixSet\\}", this.currentTileMatrixSet.identifier)
798 .replaceAll("\\{TileMatrix\\}", tileMatrix.identifier)
799 .replaceAll("\\{TileRow\\}", Integer.toString(tiley))
800 .replaceAll("\\{TileCol\\}", Integer.toString(tilex))
801 .replaceAll("(?i)\\{style\\}", this.currentLayer.style);
802
803 for (Dimension d : currentLayer.dimensions) {
804 url = url.replaceAll("(?i)\\{"+d.identifier+"\\}", d.defaultValue);
805 }
806
807 return url;
808 }
809
810 /**
811 *
812 * @param zoom zoom level
813 * @return TileMatrix that's working on this zoom level
814 */
815 private TileMatrix getTileMatrix(int zoom) {
816 if (zoom > getMaxZoom()) {
817 return null;
818 }
819 if (zoom < 0) {
820 return null;
821 }
822 return this.currentTileMatrixSet.tileMatrix.get(zoom);
823 }
824
825 @Override
826 public double getDistance(double lat1, double lon1, double lat2, double lon2) {
827 throw new UnsupportedOperationException("Not implemented");
828 }
829
830 @Override
831 public ICoordinate tileXYToLatLon(Tile tile) {
832 return tileXYToLatLon(tile.getXtile(), tile.getYtile(), tile.getZoom());
833 }
834
835 @Override
836 public ICoordinate tileXYToLatLon(TileXY xy, int zoom) {
837 return tileXYToLatLon(xy.getXIndex(), xy.getYIndex(), zoom);
838 }
839
840 @Override
841 public ICoordinate tileXYToLatLon(int x, int y, int zoom) {
842 TileMatrix matrix = getTileMatrix(zoom);
843 if (matrix == null) {
844 return CoordinateConversion.llToCoor(tileProjection.getWorldBoundsLatLon().getCenter());
845 }
846 double scale = matrix.scaleDenominator * this.crsScale;
847 EastNorth ret = new EastNorth(matrix.topLeftCorner.east() + x * scale, matrix.topLeftCorner.north() - y * scale);
848 return CoordinateConversion.llToCoor(tileProjection.eastNorth2latlon(ret));
849 }
850
851 @Override
852 public TileXY latLonToTileXY(double lat, double lon, int zoom) {
853 TileMatrix matrix = getTileMatrix(zoom);
854 if (matrix == null) {
855 return new TileXY(0, 0);
856 }
857
858 EastNorth enPoint = tileProjection.latlon2eastNorth(new LatLon(lat, lon));
859 double scale = matrix.scaleDenominator * this.crsScale;
860 return new TileXY(
861 (enPoint.east() - matrix.topLeftCorner.east()) / scale,
862 (matrix.topLeftCorner.north() - enPoint.north()) / scale
863 );
864 }
865
866 @Override
867 public TileXY latLonToTileXY(ICoordinate point, int zoom) {
868 return latLonToTileXY(point.getLat(), point.getLon(), zoom);
869 }
870
871 @Override
872 public int getTileXMax(int zoom) {
873 return getTileXMax(zoom, tileProjection);
874 }
875
876 @Override
877 public int getTileYMax(int zoom) {
878 return getTileYMax(zoom, tileProjection);
879 }
880
881 @Override
882 public Point latLonToXY(double lat, double lon, int zoom) {
883 TileMatrix matrix = getTileMatrix(zoom);
884 if (matrix == null) {
885 return new Point(0, 0);
886 }
887 double scale = matrix.scaleDenominator * this.crsScale;
888 EastNorth point = tileProjection.latlon2eastNorth(new LatLon(lat, lon));
889 return new Point(
890 (int) Math.round((point.east() - matrix.topLeftCorner.east()) / scale),
891 (int) Math.round((matrix.topLeftCorner.north() - point.north()) / scale)
892 );
893 }
894
895 @Override
896 public Point latLonToXY(ICoordinate point, int zoom) {
897 return latLonToXY(point.getLat(), point.getLon(), zoom);
898 }
899
900 @Override
901 public Coordinate xyToLatLon(Point point, int zoom) {
902 return xyToLatLon(point.x, point.y, zoom);
903 }
904
905 @Override
906 public Coordinate xyToLatLon(int x, int y, int zoom) {
907 TileMatrix matrix = getTileMatrix(zoom);
908 if (matrix == null) {
909 return new Coordinate(0, 0);
910 }
911 double scale = matrix.scaleDenominator * this.crsScale;
912 EastNorth ret = new EastNorth(
913 matrix.topLeftCorner.east() + x * scale,
914 matrix.topLeftCorner.north() - y * scale
915 );
916 LatLon ll = tileProjection.eastNorth2latlon(ret);
917 return new Coordinate(ll.lat(), ll.lon());
918 }
919
920 @Override
921 public Map<String, String> getHeaders() {
922 return headers;
923 }
924
925 @Override
926 public int getMaxZoom() {
927 if (this.currentTileMatrixSet != null) {
928 return this.currentTileMatrixSet.tileMatrix.size()-1;
929 }
930 return 0;
931 }
932
933 @Override
934 public String getTileId(int zoom, int tilex, int tiley) {
935 return getTileUrl(zoom, tilex, tiley);
936 }
937
938 /**
939 * Checks if url is acceptable by this Tile Source
940 * @param url URL to check
941 */
942 public static void checkUrl(String url) {
943 CheckParameterUtil.ensureParameterNotNull(url, "url");
944 Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);
945 while (m.find()) {
946 boolean isSupportedPattern = false;
947 for (String pattern : ALL_PATTERNS) {
948 if (m.group().matches(pattern)) {
949 isSupportedPattern = true;
950 break;
951 }
952 }
953 if (!isSupportedPattern) {
954 throw new IllegalArgumentException(
955 tr("{0} is not a valid WMS argument. Please check this server URL:\n{1}", m.group(), url));
956 }
957 }
958 }
959
960 /**
961 * @param layers to be grouped
962 * @return list with entries - grouping identifier + list of layers
963 */
964 public static List<Entry<String, List<Layer>>> groupLayersByNameAndTileMatrixSet(Collection<Layer> layers) {
965 Map<String, List<Layer>> layerByName = layers.stream().collect(
966 Collectors.groupingBy(x -> x.identifier + '\u001c' + x.tileMatrixSet.identifier));
967 return layerByName.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList());
968 }
969
970
971 /**
972 * @return set of projection codes that this TileSource supports
973 */
974 public Collection<String> getSupportedProjections() {
975 Collection<String> ret = new LinkedHashSet<>();
976 if (currentLayer == null) {
977 for (Layer layer: this.layers) {
978 ret.add(layer.tileMatrixSet.crs);
979 }
980 } else {
981 for (Layer layer: this.layers) {
982 if (currentLayer.identifier.equals(layer.identifier)) {
983 ret.add(layer.tileMatrixSet.crs);
984 }
985 }
986 }
987 return ret;
988 }
989
990 private int getTileYMax(int zoom, Projection proj) {
991 TileMatrix matrix = getTileMatrix(zoom);
992 if (matrix == null) {
993 return 0;
994 }
995
996 if (matrix.matrixHeight != -1) {
997 return matrix.matrixHeight;
998 }
999
1000 double scale = matrix.scaleDenominator * this.crsScale;
1001 EastNorth min = matrix.topLeftCorner;
1002 EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax());
1003 return (int) Math.ceil(Math.abs(max.north() - min.north()) / scale);
1004 }
1005
1006 private int getTileXMax(int zoom, Projection proj) {
1007 TileMatrix matrix = getTileMatrix(zoom);
1008 if (matrix == null) {
1009 return 0;
1010 }
1011 if (matrix.matrixWidth != -1) {
1012 return matrix.matrixWidth;
1013 }
1014
1015 double scale = matrix.scaleDenominator * this.crsScale;
1016 EastNorth min = matrix.topLeftCorner;
1017 EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax());
1018 return (int) Math.ceil(Math.abs(max.east() - min.east()) / scale);
1019 }
1020
1021 /**
1022 * Get native scales of tile source.
1023 * @return {@link ScaleList} of native scales
1024 */
1025 public ScaleList getNativeScales() {
1026 return nativeScaleList;
1027 }
1028
1029 /**
1030 * Returns the tile projection.
1031 * @return the tile projection
1032 */
1033 public Projection getTileProjection() {
1034 return tileProjection;
1035 }
1036
1037 @Override
1038 public IProjected tileXYtoProjected(int x, int y, int zoom) {
1039 TileMatrix matrix = getTileMatrix(zoom);
1040 if (matrix == null) {
1041 return new Projected(0, 0);
1042 }
1043 double scale = matrix.scaleDenominator * this.crsScale;
1044 return new Projected(
1045 matrix.topLeftCorner.east() + x * scale,
1046 matrix.topLeftCorner.north() - y * scale);
1047 }
1048
1049 @Override
1050 public TileXY projectedToTileXY(IProjected projected, int zoom) {
1051 TileMatrix matrix = getTileMatrix(zoom);
1052 if (matrix == null) {
1053 return new TileXY(0, 0);
1054 }
1055 double scale = matrix.scaleDenominator * this.crsScale;
1056 return new TileXY(
1057 (projected.getEast() - matrix.topLeftCorner.east()) / scale,
1058 -(projected.getNorth() - matrix.topLeftCorner.north()) / scale);
1059 }
1060
1061 private EastNorth tileToEastNorth(int x, int y, int z) {
1062 return CoordinateConversion.projToEn(this.tileXYtoProjected(x, y, z));
1063 }
1064
1065 private ProjectionBounds getTileProjectionBounds(Tile tile) {
1066 ProjectionBounds pb = new ProjectionBounds(tileToEastNorth(tile.getXtile(), tile.getYtile(), tile.getZoom()));
1067 pb.extend(tileToEastNorth(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom()));
1068 return pb;
1069 }
1070
1071 @Override
1072 public boolean isInside(Tile inner, Tile outer) {
1073 ProjectionBounds pbInner = getTileProjectionBounds(inner);
1074 ProjectionBounds pbOuter = getTileProjectionBounds(outer);
1075 // a little tolerance, for when inner tile touches the border of the outer tile
1076 double epsilon = 1e-7 * (pbOuter.maxEast - pbOuter.minEast);
1077 return pbOuter.minEast <= pbInner.minEast + epsilon &&
1078 pbOuter.minNorth <= pbInner.minNorth + epsilon &&
1079 pbOuter.maxEast >= pbInner.maxEast - epsilon &&
1080 pbOuter.maxNorth >= pbInner.maxNorth - epsilon;
1081 }
1082
1083 @Override
1084 public TileRange getCoveringTileRange(Tile tile, int newZoom) {
1085 TileMatrix matrixNew = getTileMatrix(newZoom);
1086 if (matrixNew == null) {
1087 return new TileRange(new TileXY(0, 0), new TileXY(0, 0), newZoom);
1088 }
1089 IProjected p0 = tileXYtoProjected(tile.getXtile(), tile.getYtile(), tile.getZoom());
1090 IProjected p1 = tileXYtoProjected(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
1091 TileXY tMin = projectedToTileXY(p0, newZoom);
1092 TileXY tMax = projectedToTileXY(p1, newZoom);
1093 // shrink the target tile a little, so we don't get neighboring tiles, that
1094 // share an edge, but don't actually cover the target tile
1095 double epsilon = 1e-7 * (tMax.getX() - tMin.getX());
1096 int minX = (int) Math.floor(tMin.getX() + epsilon);
1097 int minY = (int) Math.floor(tMin.getY() + epsilon);
1098 int maxX = (int) Math.ceil(tMax.getX() - epsilon) - 1;
1099 int maxY = (int) Math.ceil(tMax.getY() - epsilon) - 1;
1100 return new TileRange(new TileXY(minX, minY), new TileXY(maxX, maxY), newZoom);
1101 }
1102
1103 @Override
1104 public String getServerCRS() {
1105 return tileProjection != null ? tileProjection.toCode() : null;
1106 }
1107}
Note: See TracBrowser for help on using the repository browser.