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

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

see #15880 - robustness to unsupported projections

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