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

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

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