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

Last change on this file since 11790 was 11790, checked in by bastiK, 7 years ago

WMTS: fix case with multiple tilesets per projection and layer (regression from [11788]), see #7427, see #10623)

  • Property svn:eol-style set to native
File size: 37.2 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.GridBagLayout;
7import java.awt.Point;
8import java.io.ByteArrayInputStream;
9import java.io.IOException;
10import java.io.InputStream;
11import java.nio.charset.StandardCharsets;
12import java.util.ArrayList;
13import java.util.Arrays;
14import java.util.Collection;
15import java.util.Collections;
16import java.util.HashSet;
17import java.util.List;
18import java.util.Map;
19import java.util.Map.Entry;
20import java.util.Objects;
21import java.util.Optional;
22import java.util.Set;
23import java.util.SortedSet;
24import java.util.Stack;
25import java.util.TreeSet;
26import java.util.concurrent.ConcurrentHashMap;
27import java.util.function.Predicate;
28import java.util.regex.Matcher;
29import java.util.regex.Pattern;
30import java.util.stream.Collectors;
31
32import javax.imageio.ImageIO;
33import javax.swing.JPanel;
34import javax.swing.JScrollPane;
35import javax.swing.JTable;
36import javax.swing.ListSelectionModel;
37import javax.swing.table.AbstractTableModel;
38import javax.xml.namespace.QName;
39import javax.xml.stream.XMLStreamException;
40import javax.xml.stream.XMLStreamReader;
41
42import org.openstreetmap.gui.jmapviewer.Coordinate;
43import org.openstreetmap.gui.jmapviewer.Tile;
44import org.openstreetmap.gui.jmapviewer.TileXY;
45import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
46import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
47import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
48import org.openstreetmap.josm.Main;
49import org.openstreetmap.josm.data.coor.EastNorth;
50import org.openstreetmap.josm.data.coor.LatLon;
51import org.openstreetmap.josm.data.projection.Projection;
52import org.openstreetmap.josm.data.projection.Projections;
53import org.openstreetmap.josm.gui.ExtendedDialog;
54import org.openstreetmap.josm.gui.layer.NativeScaleLayer.ScaleList;
55import org.openstreetmap.josm.io.CachedFile;
56import org.openstreetmap.josm.tools.CheckParameterUtil;
57import org.openstreetmap.josm.tools.GBC;
58import org.openstreetmap.josm.tools.Utils;
59
60/**
61 * Tile Source handling WMS providers
62 *
63 * @author Wiktor Niesiobędzki
64 * @since 8526
65 */
66public class WMTSTileSource extends AbstractTMSTileSource implements TemplatedTileSource {
67 /**
68 * WMTS namespace address
69 */
70 public static final String WMTS_NS_URL = "http://www.opengis.net/wmts/1.0";
71
72 // CHECKSTYLE.OFF: SingleSpaceSeparator
73 private static final QName QN_CONTENTS = new QName(WMTSTileSource.WMTS_NS_URL, "Contents");
74 private static final QName QN_FORMAT = new QName(WMTSTileSource.WMTS_NS_URL, "Format");
75 private static final QName QN_LAYER = new QName(WMTSTileSource.WMTS_NS_URL, "Layer");
76 private static final QName QN_MATRIX_WIDTH = new QName(WMTSTileSource.WMTS_NS_URL, "MatrixWidth");
77 private static final QName QN_MATRIX_HEIGHT = new QName(WMTSTileSource.WMTS_NS_URL, "MatrixHeight");
78 private static final QName QN_RESOURCE_URL = new QName(WMTSTileSource.WMTS_NS_URL, "ResourceURL");
79 private static final QName QN_SCALE_DENOMINATOR = new QName(WMTSTileSource.WMTS_NS_URL, "ScaleDenominator");
80 private static final QName QN_STYLE = new QName(WMTSTileSource.WMTS_NS_URL, "Style");
81 private static final QName QN_TILEMATRIX = new QName(WMTSTileSource.WMTS_NS_URL, "TileMatrix");
82 private static final QName QN_TILEMATRIXSET = new QName(WMTSTileSource.WMTS_NS_URL, "TileMatrixSet");
83 private static final QName QN_TILEMATRIX_SET_LINK = new QName(WMTSTileSource.WMTS_NS_URL, "TileMatrixSetLink");
84 private static final QName QN_TILE_WIDTH = new QName(WMTSTileSource.WMTS_NS_URL, "TileWidth");
85 private static final QName QN_TILE_HEIGHT = new QName(WMTSTileSource.WMTS_NS_URL, "TileHeight");
86 private static final QName QN_TOPLEFT_CORNER = new QName(WMTSTileSource.WMTS_NS_URL, "TopLeftCorner");
87 // CHECKSTYLE.ON: SingleSpaceSeparator
88
89 private static final String PATTERN_HEADER = "\\{header\\(([^,]+),([^}]+)\\)\\}";
90
91 private static final String URL_GET_ENCODING_PARAMS = "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER={layer}&STYLE={style}&"
92 + "FORMAT={format}&tileMatrixSet={TileMatrixSet}&tileMatrix={TileMatrix}&tileRow={TileRow}&tileCol={TileCol}";
93
94 private static final String[] ALL_PATTERNS = {
95 PATTERN_HEADER,
96 };
97
98 private static class TileMatrix {
99 private String identifier;
100 private double scaleDenominator;
101 private EastNorth topLeftCorner;
102 private int tileWidth;
103 private int tileHeight;
104 private int matrixWidth = -1;
105 private int matrixHeight = -1;
106 }
107
108 private static class TileMatrixSetBuilder {
109 // sorted by zoom level
110 SortedSet<TileMatrix> tileMatrix = new TreeSet<>((o1, o2) -> -1 * Double.compare(o1.scaleDenominator, o2.scaleDenominator));
111 private String crs;
112 private String identifier;
113
114 TileMatrixSet build() {
115 return new TileMatrixSet(this);
116 }
117 }
118
119 private static class TileMatrixSet {
120
121 private final List<TileMatrix> tileMatrix;
122 private final String crs;
123 private final String identifier;
124
125 TileMatrixSet(TileMatrixSet tileMatrixSet) {
126 if (tileMatrixSet != null) {
127 tileMatrix = new ArrayList<>(tileMatrixSet.tileMatrix);
128 crs = tileMatrixSet.crs;
129 identifier = tileMatrixSet.identifier;
130 } else {
131 tileMatrix = Collections.emptyList();
132 crs = null;
133 identifier = null;
134 }
135 }
136
137 TileMatrixSet(TileMatrixSetBuilder builder) {
138 tileMatrix = new ArrayList<>(builder.tileMatrix);
139 crs = builder.crs;
140 identifier = builder.identifier;
141 }
142
143 }
144
145 private static class Layer {
146 private String format;
147 private String name;
148 private TileMatrixSet tileMatrixSet;
149 private String baseUrl;
150 private String style;
151 private final Collection<String> tileMatrixSetLinks = new ArrayList<>();
152
153 Layer(Layer l) {
154 Objects.requireNonNull(l);
155 format = l.format;
156 name = l.name;
157 baseUrl = l.baseUrl;
158 style = l.style;
159 tileMatrixSet = new TileMatrixSet(l.tileMatrixSet);
160 }
161
162 Layer() {
163 }
164 }
165
166 private static final class SelectLayerDialog extends ExtendedDialog {
167 private final transient List<Entry<String, List<Layer>>> layers;
168 private final JTable list;
169
170 SelectLayerDialog(Collection<Layer> layers) {
171 super(Main.parent, tr("Select WMTS layer"), new String[]{tr("Add layers"), tr("Cancel")});
172 this.layers = groupLayersByName(layers);
173 //getLayersTable(layers, Main.getProjection())
174 this.list = new JTable(
175 new AbstractTableModel() {
176 @Override
177 public Object getValueAt(int rowIndex, int columnIndex) {
178 switch (columnIndex) {
179 case 0:
180 return SelectLayerDialog.this.layers.get(rowIndex).getValue()
181 .stream()
182 .map(x -> x.name)
183 .collect(Collectors.joining(", ")); //this should be only one
184 case 1:
185 return SelectLayerDialog.this.layers.get(rowIndex).getValue()
186 .stream()
187 .map(x -> x.tileMatrixSet.crs)
188 .collect(Collectors.joining(", "));
189 case 2:
190 return SelectLayerDialog.this.layers.get(rowIndex).getValue()
191 .stream()
192 .map(x -> x.tileMatrixSet.identifier)
193 .collect(Collectors.joining(", ")); //this should be only one
194 default:
195 throw new IllegalArgumentException();
196 }
197 }
198
199 @Override
200 public int getRowCount() {
201 return SelectLayerDialog.this.layers.size();
202 }
203
204 @Override
205 public int getColumnCount() {
206 return 3;
207 }
208
209 @Override
210 public String getColumnName(int column) {
211 switch (column) {
212 case 0: return tr("Layer name");
213 case 1: return tr("Projection");
214 case 2: return tr("Matrix set identifier");
215 default:
216 throw new IllegalArgumentException();
217 }
218 }
219 });
220 this.list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
221 this.list.setRowSelectionAllowed(true);
222 this.list.setColumnSelectionAllowed(false);
223 JPanel panel = new JPanel(new GridBagLayout());
224 panel.add(new JScrollPane(this.list), GBC.eol().fill());
225 setContent(panel);
226 }
227
228 public DefaultLayer getSelectedLayer() {
229 int index = list.getSelectedRow();
230 if (index < 0) {
231 return null; //nothing selected
232 }
233 Layer selectedLayer = layers.get(index).getValue().get(0);
234 return new WMTSDefaultLayer(selectedLayer.name, selectedLayer.tileMatrixSet.identifier);
235 }
236 }
237
238 private final Map<String, String> headers = new ConcurrentHashMap<>();
239 private final Collection<Layer> layers;
240 private Layer currentLayer;
241 private TileMatrixSet currentTileMatrixSet;
242 private double crsScale;
243 private GetCapabilitiesParseHelper.TransferMode transferMode;
244
245 private ScaleList nativeScaleList;
246
247 private final WMTSDefaultLayer defaultLayer;
248
249
250 /**
251 * Creates a tile source based on imagery info
252 * @param info imagery info
253 * @throws IOException if any I/O error occurs
254 * @throws IllegalArgumentException if any other error happens for the given imagery info
255 */
256 public WMTSTileSource(ImageryInfo info) throws IOException {
257 super(info);
258 CheckParameterUtil.ensureThat(info.getDefaultLayers().size() < 2, "At most 1 default layer for WMTS is supported");
259
260 this.baseUrl = GetCapabilitiesParseHelper.normalizeCapabilitiesUrl(handleTemplate(info.getUrl()));
261 this.layers = getCapabilities();
262 this.defaultLayer = info.getDefaultLayers().isEmpty() ? null : (WMTSDefaultLayer) info.getDefaultLayers().iterator().next();
263 if (this.layers.isEmpty())
264 throw new IllegalArgumentException(tr("No layers defined by getCapabilities document: {0}", info.getUrl()));
265 }
266
267 /**
268 * Creates a dialog based on this tile source with all available layers and returns the name of selected layer
269 * @return Name of selected layer
270 */
271 public DefaultLayer userSelectLayer() {
272 Collection<Entry<String, List<Layer>>> grouppedLayers = groupLayersByName(layers);
273
274 // if there is only one layer name no point in asking
275 if (grouppedLayers.size() == 1) {
276 Layer selectedLayer = grouppedLayers.iterator().next().getValue().get(0);
277 return new WMTSDefaultLayer(selectedLayer.name, selectedLayer.tileMatrixSet.identifier);
278 }
279
280 final SelectLayerDialog layerSelection = new SelectLayerDialog(layers);
281 if (layerSelection.showDialog().getValue() == 1) {
282 return layerSelection.getSelectedLayer();
283 }
284 return null;
285 }
286
287 private String handleTemplate(String url) {
288 Pattern pattern = Pattern.compile(PATTERN_HEADER);
289 StringBuffer output = new StringBuffer();
290 Matcher matcher = pattern.matcher(url);
291 while (matcher.find()) {
292 this.headers.put(matcher.group(1), matcher.group(2));
293 matcher.appendReplacement(output, "");
294 }
295 matcher.appendTail(output);
296 return output.toString();
297 }
298
299 private static List<Entry<String, List<Layer>>> groupLayersByName(Collection<Layer> layers) {
300 Map<String, List<Layer>> layerByName = layers.stream().collect(
301 Collectors.groupingBy(x -> x.name + '\u001c' + x.tileMatrixSet.identifier));
302 return layerByName.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList());
303 }
304
305 /**
306 * @return capabilities
307 * @throws IOException in case of any I/O error
308 * @throws IllegalArgumentException in case of any other error
309 */
310 private Collection<Layer> getCapabilities() throws IOException {
311 try (CachedFile cf = new CachedFile(baseUrl); InputStream in = cf.setHttpHeaders(headers).
312 setMaxAge(7 * CachedFile.DAYS).
313 setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince).
314 getInputStream()) {
315 byte[] data = Utils.readBytesFromStream(in);
316 if (data.length == 0) {
317 cf.clear();
318 throw new IllegalArgumentException("Could not read data from: " + baseUrl);
319 }
320
321 try {
322 XMLStreamReader reader = GetCapabilitiesParseHelper.getReader(new ByteArrayInputStream(data));
323 Collection<Layer> ret = new ArrayList<>();
324 for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) {
325 if (event == XMLStreamReader.START_ELEMENT) {
326 if (GetCapabilitiesParseHelper.QN_OWS_OPERATIONS_METADATA.equals(reader.getName())) {
327 parseOperationMetadata(reader);
328 }
329
330 if (QN_CONTENTS.equals(reader.getName())) {
331 ret = parseContents(reader);
332 }
333 }
334 }
335 return ret;
336 } catch (XMLStreamException e) {
337 cf.clear();
338 Main.warn(new String(data, StandardCharsets.UTF_8));
339 throw new IllegalArgumentException(e);
340 }
341 }
342 }
343
344 /**
345 * Parse Contents tag. Returns when reader reaches Contents closing tag
346 *
347 * @param reader StAX reader instance
348 * @return collection of layers within contents with properly linked TileMatrixSets
349 * @throws XMLStreamException See {@link XMLStreamReader}
350 */
351 private static Collection<Layer> parseContents(XMLStreamReader reader) throws XMLStreamException {
352 Map<String, TileMatrixSet> matrixSetById = new ConcurrentHashMap<>();
353 Collection<Layer> layers = new ArrayList<>();
354 for (int event = reader.getEventType();
355 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_CONTENTS.equals(reader.getName()));
356 event = reader.next()) {
357 if (event == XMLStreamReader.START_ELEMENT) {
358 if (QN_LAYER.equals(reader.getName())) {
359 Layer l = parseLayer(reader);
360 if (l != null) {
361 layers.add(l);
362 }
363 }
364 if (QN_TILEMATRIXSET.equals(reader.getName())) {
365 TileMatrixSet entry = parseTileMatrixSet(reader);
366 matrixSetById.put(entry.identifier, entry);
367 }
368 }
369 }
370 Collection<Layer> ret = new ArrayList<>();
371 // link layers to matrix sets
372 for (Layer l: layers) {
373 for (String tileMatrixId: l.tileMatrixSetLinks) {
374 Layer newLayer = new Layer(l); // create a new layer object for each tile matrix set supported
375 newLayer.tileMatrixSet = matrixSetById.get(tileMatrixId);
376 ret.add(newLayer);
377 }
378 }
379 return ret;
380 }
381
382 /**
383 * Parse Layer tag. Returns when reader will reach Layer closing tag
384 *
385 * @param reader StAX reader instance
386 * @return Layer object, with tileMatrixSetLinks and no tileMatrixSet attribute set.
387 * @throws XMLStreamException See {@link XMLStreamReader}
388 */
389 private static Layer parseLayer(XMLStreamReader reader) throws XMLStreamException {
390 Layer layer = new Layer();
391 Stack<QName> tagStack = new Stack<>();
392 List<String> supportedMimeTypes = Arrays.asList(ImageIO.getReaderMIMETypes());
393 Collection<String> unsupportedFormats = new ArrayList<>();
394
395 for (int event = reader.getEventType();
396 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_LAYER.equals(reader.getName()));
397 event = reader.next()) {
398 if (event == XMLStreamReader.START_ELEMENT) {
399 tagStack.push(reader.getName());
400 if (tagStack.size() == 2) {
401 if (QN_FORMAT.equals(reader.getName())) {
402 String format = reader.getElementText();
403 if (supportedMimeTypes.contains(format)) {
404 layer.format = format;
405 } else {
406 unsupportedFormats.add(format);
407 }
408 } else if (GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER.equals(reader.getName())) {
409 layer.name = reader.getElementText();
410 } else if (QN_RESOURCE_URL.equals(reader.getName()) &&
411 "tile".equals(reader.getAttributeValue("", "resourceType"))) {
412 layer.baseUrl = reader.getAttributeValue("", "template");
413 } else if (QN_STYLE.equals(reader.getName()) &&
414 "true".equals(reader.getAttributeValue("", "isDefault"))) {
415 if (GetCapabilitiesParseHelper.moveReaderToTag(reader, new QName[] {GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER})) {
416 layer.style = reader.getElementText();
417 tagStack.push(reader.getName()); // keep tagStack in sync
418 }
419 } else if (QN_TILEMATRIX_SET_LINK.equals(reader.getName())) {
420 layer.tileMatrixSetLinks.add(praseTileMatrixSetLink(reader));
421 } else {
422 GetCapabilitiesParseHelper.moveReaderToEndCurrentTag(reader);
423 }
424 }
425 }
426 // need to get event type from reader, as parsing might have change position of reader
427 if (reader.getEventType() == XMLStreamReader.END_ELEMENT) {
428 QName start = tagStack.pop();
429 if (!start.equals(reader.getName())) {
430 throw new IllegalStateException(tr("WMTS Parser error - start element {0} has different name than end element {2}",
431 start, reader.getName()));
432 }
433 }
434 }
435 if (layer.style == null) {
436 layer.style = "";
437 }
438 if (layer.format == null) {
439 // no format found - it's mandatory parameter - can't use this layer
440 Main.warn(tr("Can''t use layer {0} because no supported formats where found. Layer is available in formats: {1}",
441 layer.name,
442 String.join(", ", unsupportedFormats)));
443 return null;
444 }
445 return layer;
446 }
447
448 /**
449 * Gets TileMatrixSetLink value. Returns when reader is on TileMatrixSetLink closing tag
450 *
451 * @param reader StAX reader instance
452 * @return TileMatrixSetLink identifier
453 * @throws XMLStreamException See {@link XMLStreamReader}
454 */
455 private static String praseTileMatrixSetLink(XMLStreamReader reader) throws XMLStreamException {
456 String ret = null;
457 for (int event = reader.getEventType();
458 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT &&
459 QN_TILEMATRIX_SET_LINK.equals(reader.getName()));
460 event = reader.next()) {
461 if (event == XMLStreamReader.START_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName())) {
462 ret = reader.getElementText();
463 }
464 }
465 return ret;
466 }
467
468 /**
469 * Parses TileMatrixSet section. Returns when reader is on TileMatrixSet closing tag
470 * @param reader StAX reader instance
471 * @return TileMatrixSet object
472 * @throws XMLStreamException See {@link XMLStreamReader}
473 */
474 private static TileMatrixSet parseTileMatrixSet(XMLStreamReader reader) throws XMLStreamException {
475 TileMatrixSetBuilder matrixSet = new TileMatrixSetBuilder();
476 for (int event = reader.getEventType();
477 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName()));
478 event = reader.next()) {
479 if (event == XMLStreamReader.START_ELEMENT) {
480 if (GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER.equals(reader.getName())) {
481 matrixSet.identifier = reader.getElementText();
482 }
483 if (GetCapabilitiesParseHelper.QN_OWS_SUPPORTED_CRS.equals(reader.getName())) {
484 matrixSet.crs = GetCapabilitiesParseHelper.crsToCode(reader.getElementText());
485 }
486 if (QN_TILEMATRIX.equals(reader.getName())) {
487 matrixSet.tileMatrix.add(parseTileMatrix(reader, matrixSet.crs));
488 }
489 }
490 }
491 return matrixSet.build();
492 }
493
494 /**
495 * Parses TileMatrix section. Returns when reader is on TileMatrix closing tag.
496 * @param reader StAX reader instance
497 * @param matrixCrs projection used by this matrix
498 * @return TileMatrix object
499 * @throws XMLStreamException See {@link XMLStreamReader}
500 */
501 private static TileMatrix parseTileMatrix(XMLStreamReader reader, String matrixCrs) throws XMLStreamException {
502 Projection matrixProj = Optional.ofNullable(Projections.getProjectionByCode(matrixCrs))
503 .orElseGet(Main::getProjection); // use current projection if none found. Maybe user is using custom string
504 TileMatrix ret = new TileMatrix();
505 for (int event = reader.getEventType();
506 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_TILEMATRIX.equals(reader.getName()));
507 event = reader.next()) {
508 if (event == XMLStreamReader.START_ELEMENT) {
509 if (GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER.equals(reader.getName())) {
510 ret.identifier = reader.getElementText();
511 }
512 if (QN_SCALE_DENOMINATOR.equals(reader.getName())) {
513 ret.scaleDenominator = Double.parseDouble(reader.getElementText());
514 }
515 if (QN_TOPLEFT_CORNER.equals(reader.getName())) {
516 String[] topLeftCorner = reader.getElementText().split(" ");
517 if (matrixProj.switchXY()) {
518 ret.topLeftCorner = new EastNorth(Double.parseDouble(topLeftCorner[1]), Double.parseDouble(topLeftCorner[0]));
519 } else {
520 ret.topLeftCorner = new EastNorth(Double.parseDouble(topLeftCorner[0]), Double.parseDouble(topLeftCorner[1]));
521 }
522 }
523 if (QN_TILE_HEIGHT.equals(reader.getName())) {
524 ret.tileHeight = Integer.parseInt(reader.getElementText());
525 }
526 if (QN_TILE_WIDTH.equals(reader.getName())) {
527 ret.tileWidth = Integer.parseInt(reader.getElementText());
528 }
529 if (QN_MATRIX_HEIGHT.equals(reader.getName())) {
530 ret.matrixHeight = Integer.parseInt(reader.getElementText());
531 }
532 if (QN_MATRIX_WIDTH.equals(reader.getName())) {
533 ret.matrixWidth = Integer.parseInt(reader.getElementText());
534 }
535 }
536 }
537 if (ret.tileHeight != ret.tileWidth) {
538 throw new AssertionError(tr("Only square tiles are supported. {0}x{1} returned by server for TileMatrix identifier {2}",
539 ret.tileHeight, ret.tileWidth, ret.identifier));
540 }
541 return ret;
542 }
543
544 /**
545 * Parses OperationMetadata section. Returns when reader is on OperationsMetadata closing tag.
546 * Sets this.baseUrl and this.transferMode
547 *
548 * @param reader StAX reader instance
549 * @throws XMLStreamException See {@link XMLStreamReader}
550 */
551 private void parseOperationMetadata(XMLStreamReader reader) throws XMLStreamException {
552 for (int event = reader.getEventType();
553 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT &&
554 GetCapabilitiesParseHelper.QN_OWS_OPERATIONS_METADATA.equals(reader.getName()));
555 event = reader.next()) {
556 if (event == XMLStreamReader.START_ELEMENT &&
557 GetCapabilitiesParseHelper.QN_OWS_OPERATION.equals(reader.getName()) &&
558 "GetTile".equals(reader.getAttributeValue("", "name")) &&
559 GetCapabilitiesParseHelper.moveReaderToTag(reader, new QName[] {
560 GetCapabilitiesParseHelper.QN_OWS_DCP,
561 GetCapabilitiesParseHelper.QN_OWS_HTTP,
562 GetCapabilitiesParseHelper.QN_OWS_GET,
563 })) {
564 this.baseUrl = reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href");
565 this.transferMode = GetCapabilitiesParseHelper.getTransferMode(reader);
566 }
567 }
568 }
569
570 /**
571 * Initializes projection for this TileSource with projection
572 * @param proj projection to be used by this TileSource
573 */
574 public void initProjection(Projection proj) {
575 // getLayers will return only layers matching the name, if the user already choose the layer
576 // so we will not ask the user again to chose the layer, if he just changes projection
577 Collection<Layer> candidates = getLayers(
578 currentLayer != null ? new WMTSDefaultLayer(currentLayer.name, currentLayer.tileMatrixSet.identifier) : defaultLayer,
579 proj.toCode());
580
581 if (candidates.size() > 1 && defaultLayer != null) {
582 candidates = candidates.stream()
583 .filter(t -> t.tileMatrixSet.identifier.equals(defaultLayer.getTileMatrixSet()))
584 .collect(Collectors.toList());
585 }
586 if (candidates.size() == 1) {
587 Layer newLayer = candidates.iterator().next();
588 if (newLayer != null) {
589 this.currentTileMatrixSet = newLayer.tileMatrixSet;
590 this.currentLayer = newLayer;
591 Collection<Double> scales = new ArrayList<>(currentTileMatrixSet.tileMatrix.size());
592 for (TileMatrix tileMatrix : currentTileMatrixSet.tileMatrix) {
593 scales.add(tileMatrix.scaleDenominator * 0.28e-03);
594 }
595 this.nativeScaleList = new ScaleList(scales);
596 }
597 } else if (candidates.size() > 1) {
598 Main.warn("More than one layer WMTS available: {0} for projection {1} and name {2}. Do not know which to process",
599 candidates.stream().map(x -> x.name + ": " + x.tileMatrixSet.identifier).collect(Collectors.joining(", ")),
600 proj.toCode(),
601 currentLayer != null ? currentLayer.name : defaultLayer
602 );
603 }
604 this.crsScale = getTileSize() * 0.28e-03 / proj.getMetersPerUnit();
605 }
606
607 /**
608 *
609 * @param searchLayer which layer do we look for
610 * @param projectionCode projection code to match
611 * @return Collection of layers matching the name of the layer and projection, or only projection if name is not provided
612 */
613 private Collection<Layer> getLayers(WMTSDefaultLayer searchLayer, String projectionCode) {
614 Collection<Layer> ret = new ArrayList<>();
615 if (this.layers != null) {
616 for (Layer layer: this.layers) {
617 if ((searchLayer == null || (// if it's null, then accept all layers
618 searchLayer.getLayerName().equals(layer.name)))
619 && (projectionCode == null || // if it's null, then accept any projection
620 projectionCode.equals(layer.tileMatrixSet.crs))) {
621 ret.add(layer);
622 }
623 }
624 }
625 return ret;
626 }
627
628 @Override
629 public int getTileSize() {
630 // no support for non-square tiles (tileHeight != tileWidth)
631 // and for different tile sizes at different zoom levels
632 Collection<Layer> projLayers = getLayers(null, Main.getProjection().toCode());
633 if (!projLayers.isEmpty()) {
634 return projLayers.iterator().next().tileMatrixSet.tileMatrix.get(0).tileHeight;
635 }
636 // if no layers is found, fallback to default mercator tile size. Maybe it will work
637 Main.warn("WMTS: Could not determine tile size. Using default tile size of: {0}", getDefaultTileSize());
638 return getDefaultTileSize();
639 }
640
641 @Override
642 public String getTileUrl(int zoom, int tilex, int tiley) {
643 if (currentLayer == null) {
644 return "";
645 }
646
647 String url;
648 if (currentLayer.baseUrl != null && transferMode == null) {
649 url = currentLayer.baseUrl;
650 } else {
651 switch (transferMode) {
652 case KVP:
653 url = baseUrl + URL_GET_ENCODING_PARAMS;
654 break;
655 case REST:
656 url = currentLayer.baseUrl;
657 break;
658 default:
659 url = "";
660 break;
661 }
662 }
663
664 TileMatrix tileMatrix = getTileMatrix(zoom);
665
666 if (tileMatrix == null) {
667 return ""; // no matrix, probably unsupported CRS selected.
668 }
669
670 return url.replaceAll("\\{layer\\}", this.currentLayer.name)
671 .replaceAll("\\{format\\}", this.currentLayer.format)
672 .replaceAll("\\{TileMatrixSet\\}", this.currentTileMatrixSet.identifier)
673 .replaceAll("\\{TileMatrix\\}", tileMatrix.identifier)
674 .replaceAll("\\{TileRow\\}", Integer.toString(tiley))
675 .replaceAll("\\{TileCol\\}", Integer.toString(tilex))
676 .replaceAll("(?i)\\{style\\}", this.currentLayer.style);
677 }
678
679 /**
680 *
681 * @param zoom zoom level
682 * @return TileMatrix that's working on this zoom level
683 */
684 private TileMatrix getTileMatrix(int zoom) {
685 if (zoom > getMaxZoom()) {
686 return null;
687 }
688 if (zoom < 0) {
689 return null;
690 }
691 return this.currentTileMatrixSet.tileMatrix.get(zoom);
692 }
693
694 @Override
695 public double getDistance(double lat1, double lon1, double lat2, double lon2) {
696 throw new UnsupportedOperationException("Not implemented");
697 }
698
699 @Override
700 public ICoordinate tileXYToLatLon(Tile tile) {
701 return tileXYToLatLon(tile.getXtile(), tile.getYtile(), tile.getZoom());
702 }
703
704 @Override
705 public ICoordinate tileXYToLatLon(TileXY xy, int zoom) {
706 return tileXYToLatLon(xy.getXIndex(), xy.getYIndex(), zoom);
707 }
708
709 @Override
710 public ICoordinate tileXYToLatLon(int x, int y, int zoom) {
711 TileMatrix matrix = getTileMatrix(zoom);
712 if (matrix == null) {
713 return Main.getProjection().getWorldBoundsLatLon().getCenter().toCoordinate();
714 }
715 double scale = matrix.scaleDenominator * this.crsScale;
716 EastNorth ret = new EastNorth(matrix.topLeftCorner.east() + x * scale, matrix.topLeftCorner.north() - y * scale);
717 return Main.getProjection().eastNorth2latlon(ret).toCoordinate();
718 }
719
720 @Override
721 public TileXY latLonToTileXY(double lat, double lon, int zoom) {
722 TileMatrix matrix = getTileMatrix(zoom);
723 if (matrix == null) {
724 return new TileXY(0, 0);
725 }
726
727 Projection proj = Main.getProjection();
728 EastNorth enPoint = proj.latlon2eastNorth(new LatLon(lat, lon));
729 double scale = matrix.scaleDenominator * this.crsScale;
730 return new TileXY(
731 (enPoint.east() - matrix.topLeftCorner.east()) / scale,
732 (matrix.topLeftCorner.north() - enPoint.north()) / scale
733 );
734 }
735
736 @Override
737 public TileXY latLonToTileXY(ICoordinate point, int zoom) {
738 return latLonToTileXY(point.getLat(), point.getLon(), zoom);
739 }
740
741 @Override
742 public int getTileXMax(int zoom) {
743 return getTileXMax(zoom, Main.getProjection());
744 }
745
746 @Override
747 public int getTileYMax(int zoom) {
748 return getTileYMax(zoom, Main.getProjection());
749 }
750
751 @Override
752 public Point latLonToXY(double lat, double lon, int zoom) {
753 TileMatrix matrix = getTileMatrix(zoom);
754 if (matrix == null) {
755 return new Point(0, 0);
756 }
757 double scale = matrix.scaleDenominator * this.crsScale;
758 EastNorth point = Main.getProjection().latlon2eastNorth(new LatLon(lat, lon));
759 return new Point(
760 (int) Math.round((point.east() - matrix.topLeftCorner.east()) / scale),
761 (int) Math.round((matrix.topLeftCorner.north() - point.north()) / scale)
762 );
763 }
764
765 @Override
766 public Point latLonToXY(ICoordinate point, int zoom) {
767 return latLonToXY(point.getLat(), point.getLon(), zoom);
768 }
769
770 @Override
771 public Coordinate xyToLatLon(Point point, int zoom) {
772 return xyToLatLon(point.x, point.y, zoom);
773 }
774
775 @Override
776 public Coordinate xyToLatLon(int x, int y, int zoom) {
777 TileMatrix matrix = getTileMatrix(zoom);
778 if (matrix == null) {
779 return new Coordinate(0, 0);
780 }
781 double scale = matrix.scaleDenominator * this.crsScale;
782 Projection proj = Main.getProjection();
783 EastNorth ret = new EastNorth(
784 matrix.topLeftCorner.east() + x * scale,
785 matrix.topLeftCorner.north() - y * scale
786 );
787 LatLon ll = proj.eastNorth2latlon(ret);
788 return new Coordinate(ll.lat(), ll.lon());
789 }
790
791 @Override
792 public Map<String, String> getHeaders() {
793 return headers;
794 }
795
796 @Override
797 public int getMaxZoom() {
798 if (this.currentTileMatrixSet != null) {
799 return this.currentTileMatrixSet.tileMatrix.size()-1;
800 }
801 return 0;
802 }
803
804 @Override
805 public String getTileId(int zoom, int tilex, int tiley) {
806 return getTileUrl(zoom, tilex, tiley);
807 }
808
809 /**
810 * Checks if url is acceptable by this Tile Source
811 * @param url URL to check
812 */
813 public static void checkUrl(String url) {
814 CheckParameterUtil.ensureParameterNotNull(url, "url");
815 Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);
816 while (m.find()) {
817 boolean isSupportedPattern = false;
818 for (String pattern : ALL_PATTERNS) {
819 if (m.group().matches(pattern)) {
820 isSupportedPattern = true;
821 break;
822 }
823 }
824 if (!isSupportedPattern) {
825 throw new IllegalArgumentException(
826 tr("{0} is not a valid WMS argument. Please check this server URL:\n{1}", m.group(), url));
827 }
828 }
829 }
830
831 /**
832 * @return set of projection codes that this TileSource supports
833 */
834 public Set<String> getSupportedProjections() {
835 Set<String> ret = new HashSet<>();
836 if (currentLayer == null) {
837 for (Layer layer: this.layers) {
838 ret.add(layer.tileMatrixSet.crs);
839 }
840 } else {
841 for (Layer layer: this.layers) {
842 if (currentLayer.name.equals(layer.name)) {
843 ret.add(layer.tileMatrixSet.crs);
844 }
845 }
846 }
847 return ret;
848 }
849
850 private int getTileYMax(int zoom, Projection proj) {
851 TileMatrix matrix = getTileMatrix(zoom);
852 if (matrix == null) {
853 return 0;
854 }
855
856 if (matrix.matrixHeight != -1) {
857 return matrix.matrixHeight;
858 }
859
860 double scale = matrix.scaleDenominator * this.crsScale;
861 EastNorth min = matrix.topLeftCorner;
862 EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax());
863 return (int) Math.ceil(Math.abs(max.north() - min.north()) / scale);
864 }
865
866 private int getTileXMax(int zoom, Projection proj) {
867 TileMatrix matrix = getTileMatrix(zoom);
868 if (matrix == null) {
869 return 0;
870 }
871 if (matrix.matrixWidth != -1) {
872 return matrix.matrixWidth;
873 }
874
875 double scale = matrix.scaleDenominator * this.crsScale;
876 EastNorth min = matrix.topLeftCorner;
877 EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax());
878 return (int) Math.ceil(Math.abs(max.east() - min.east()) / scale);
879 }
880
881 /**
882 * Get native scales of tile source.
883 * @return {@link ScaleList} of native scales
884 */
885 public ScaleList getNativeScales() {
886 return nativeScaleList;
887 }
888
889}
Note: See TracBrowser for help on using the repository browser.