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

Last change on this file since 9887 was 9887, checked in by wiktorn, 8 years ago

Properly interpret more complex getCapabilities documents.

Keep a track of how deep into the XML tree reader has traversed. Do the
interpretation of the tags only if they are present at proper level.

If unknown tag is found, move the reader to the end of the tag, to avoid
confusion.

When using moveToReader within parseLayer, it is needed, to keep tagStack
updated.

This should fix problems with BE WMTS imagery - See: #12573

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