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

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

see #16084 - remove unused code

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