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

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

see #15229 - deprecate all Main methods related to projections. New ProjectionRegistry class

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