001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.imagery;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.GridBagLayout;
007import java.awt.Point;
008import java.io.ByteArrayInputStream;
009import java.io.IOException;
010import java.io.InputStream;
011import java.net.MalformedURLException;
012import java.net.URL;
013import java.util.ArrayList;
014import java.util.Collection;
015import java.util.Collections;
016import java.util.Comparator;
017import java.util.HashSet;
018import java.util.List;
019import java.util.Map;
020import java.util.Set;
021import java.util.SortedSet;
022import java.util.Stack;
023import java.util.TreeSet;
024import java.util.concurrent.ConcurrentHashMap;
025import java.util.regex.Matcher;
026import java.util.regex.Pattern;
027
028import javax.swing.JPanel;
029import javax.swing.JScrollPane;
030import javax.swing.JTable;
031import javax.swing.ListSelectionModel;
032import javax.swing.table.AbstractTableModel;
033import javax.xml.namespace.QName;
034import javax.xml.stream.XMLInputFactory;
035import javax.xml.stream.XMLStreamException;
036import javax.xml.stream.XMLStreamReader;
037
038import org.openstreetmap.gui.jmapviewer.Coordinate;
039import org.openstreetmap.gui.jmapviewer.Tile;
040import org.openstreetmap.gui.jmapviewer.TileXY;
041import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
042import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
043import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
044import org.openstreetmap.josm.Main;
045import org.openstreetmap.josm.data.coor.EastNorth;
046import org.openstreetmap.josm.data.coor.LatLon;
047import org.openstreetmap.josm.data.projection.Projection;
048import org.openstreetmap.josm.data.projection.Projections;
049import org.openstreetmap.josm.gui.ExtendedDialog;
050import org.openstreetmap.josm.gui.layer.NativeScaleLayer.ScaleList;
051import org.openstreetmap.josm.io.CachedFile;
052import org.openstreetmap.josm.tools.CheckParameterUtil;
053import org.openstreetmap.josm.tools.GBC;
054import org.openstreetmap.josm.tools.Utils;
055
056/**
057 * Tile Source handling WMS providers
058 *
059 * @author Wiktor Niesiobędzki
060 * @since 8526
061 */
062public class WMTSTileSource extends AbstractTMSTileSource implements TemplatedTileSource {
063    private static final String PATTERN_HEADER  = "\\{header\\(([^,]+),([^}]+)\\)\\}";
064
065    private static final String URL_GET_ENCODING_PARAMS = "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER={layer}&STYLE={style}&"
066            + "FORMAT={format}&tileMatrixSet={TileMatrixSet}&tileMatrix={TileMatrix}&tileRow={TileRow}&tileCol={TileCol}";
067
068    private static final String[] ALL_PATTERNS = {
069        PATTERN_HEADER,
070    };
071
072    private static final String OWS_NS_URL = "http://www.opengis.net/ows/1.1";
073    private static final String WMTS_NS_URL = "http://www.opengis.net/wmts/1.0";
074    private static final String XLINK_NS_URL = "http://www.w3.org/1999/xlink";
075
076    private static class TileMatrix {
077        private String identifier;
078        private double scaleDenominator;
079        private EastNorth topLeftCorner;
080        private int tileWidth;
081        private int tileHeight;
082        private int matrixWidth = -1;
083        private int matrixHeight = -1;
084    }
085
086    private static class TileMatrixSetBuilder {
087        SortedSet<TileMatrix> tileMatrix = new TreeSet<>(new Comparator<TileMatrix>() {
088            @Override
089            public int compare(TileMatrix o1, TileMatrix o2) {
090                // reverse the order, so it will be from greatest (lowest zoom level) to lowest value (highest zoom level)
091                return -1 * Double.compare(o1.scaleDenominator, o2.scaleDenominator);
092            }
093        }); // sorted by zoom level
094        private String crs;
095        private String identifier;
096
097        TileMatrixSet build() {
098            return new TileMatrixSet(this);
099        }
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                        if (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                        }
392                    } else if (new QName(WMTS_NS_URL, "TileMatrixSetLink").equals(reader.getName())) {
393                        layer.tileMatrixSetLinks.add(praseTileMatrixSetLink(reader));
394                    } else {
395                        moveReaderToEndCurrentTag(reader);
396                    }
397                }
398            }
399            // need to get event type from reader, as parsing might have change position of reader
400            if (reader.getEventType() == XMLStreamReader.END_ELEMENT) {
401                QName start = tagStack.pop();
402                if (!start.equals(reader.getName())) {
403                    throw new IllegalStateException(tr("WMTS Parser error - start element {0} has different name than end element {2}",
404                            start, reader.getName()));
405                }
406            }
407        }
408        if (layer.style == null) {
409            layer.style = "";
410        }
411        return layer;
412    }
413
414    /**
415     * Moves the reader to the closing tag of current tag.
416     * @param reader XML stream reader positioned on XMLStreamReader.START_ELEMENT
417     * @throws XMLStreamException when parse exception occurs
418     */
419    private static void moveReaderToEndCurrentTag(XMLStreamReader reader) throws XMLStreamException {
420        int level = 0;
421        QName tag = reader.getName();
422        for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) {
423            switch (event) {
424            case XMLStreamReader.START_ELEMENT:
425                level += 1;
426                break;
427            case XMLStreamReader.END_ELEMENT:
428                level -= 1;
429                if (level == 0 && tag.equals(reader.getName())) {
430                    return;
431                }
432            }
433            if (level < 0) {
434                throw new IllegalStateException("WMTS Parser error - moveReaderToEndCurrentTag failed to find closing tag");
435            }
436        }
437        throw new IllegalStateException("WMTS Parser error - moveReaderToEndCurrentTag failed to find closing tag");
438
439    }
440
441    /**
442     * Gets TileMatrixSetLink value. Returns when reader is on TileMatrixSetLink closing tag
443     *
444     * @param reader StAX reader instance
445     * @return TileMatrixSetLink identifier
446     * @throws XMLStreamException See {@link XMLStreamReader}
447     */
448    private static String praseTileMatrixSetLink(XMLStreamReader reader) throws XMLStreamException {
449        String ret = null;
450        for (int event = reader.getEventType();
451                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT &&
452                        new QName(WMTS_NS_URL, "TileMatrixSetLink").equals(reader.getName()));
453                event = reader.next()) {
454            if (event == XMLStreamReader.START_ELEMENT && new QName(WMTS_NS_URL, "TileMatrixSet").equals(reader.getName())) {
455                ret = reader.getElementText();
456            }
457        }
458        return ret;
459    }
460
461    /**
462     * Parses TileMatrixSet section. Returns when reader is on TileMatrixSet closing tag
463     * @param reader StAX reader instance
464     * @return TileMatrixSet object
465     * @throws XMLStreamException See {@link XMLStreamReader}
466     */
467    private static TileMatrixSet parseTileMatrixSet(XMLStreamReader reader) throws XMLStreamException {
468        TileMatrixSetBuilder matrixSet = new TileMatrixSetBuilder();
469        for (int event = reader.getEventType();
470                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && new QName(WMTS_NS_URL, "TileMatrixSet").equals(reader.getName()));
471                event = reader.next()) {
472                    if (event == XMLStreamReader.START_ELEMENT) {
473                        if (new QName(OWS_NS_URL, "Identifier").equals(reader.getName())) {
474                            matrixSet.identifier = reader.getElementText();
475                        }
476                        if (new QName(OWS_NS_URL, "SupportedCRS").equals(reader.getName())) {
477                            matrixSet.crs = crsToCode(reader.getElementText());
478                        }
479                        if (new QName(WMTS_NS_URL, "TileMatrix").equals(reader.getName())) {
480                            matrixSet.tileMatrix.add(parseTileMatrix(reader, matrixSet.crs));
481                        }
482                    }
483        }
484        return matrixSet.build();
485    }
486
487    /**
488     * Parses TileMatrix section. Returns when reader is on TileMatrix closing tag.
489     * @param reader StAX reader instance
490     * @param matrixCrs projection used by this matrix
491     * @return TileMatrix object
492     * @throws XMLStreamException See {@link XMLStreamReader}
493     */
494    private static TileMatrix parseTileMatrix(XMLStreamReader reader, String matrixCrs) throws XMLStreamException {
495        Projection matrixProj = Projections.getProjectionByCode(matrixCrs);
496        TileMatrix ret = new TileMatrix();
497
498        if (matrixProj == null) {
499            // use current projection if none found. Maybe user is using custom string
500            matrixProj = Main.getProjection();
501        }
502        for (int event = reader.getEventType();
503                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && new QName(WMTS_NS_URL, "TileMatrix").equals(reader.getName()));
504                event = reader.next()) {
505            if (event == XMLStreamReader.START_ELEMENT) {
506                if (new QName(OWS_NS_URL, "Identifier").equals(reader.getName())) {
507                    ret.identifier = reader.getElementText();
508                }
509                if (new QName(WMTS_NS_URL, "ScaleDenominator").equals(reader.getName())) {
510                    ret.scaleDenominator = Double.parseDouble(reader.getElementText());
511                }
512                if (new QName(WMTS_NS_URL, "TopLeftCorner").equals(reader.getName())) {
513                    String[] topLeftCorner = reader.getElementText().split(" ");
514                    if (matrixProj.switchXY()) {
515                        ret.topLeftCorner = new EastNorth(Double.parseDouble(topLeftCorner[1]), Double.parseDouble(topLeftCorner[0]));
516                    } else {
517                        ret.topLeftCorner = new EastNorth(Double.parseDouble(topLeftCorner[0]), Double.parseDouble(topLeftCorner[1]));
518                    }
519                }
520                if (new QName(WMTS_NS_URL, "TileHeight").equals(reader.getName())) {
521                    ret.tileHeight = Integer.parseInt(reader.getElementText());
522                }
523                if (new QName(WMTS_NS_URL, "TileWidth").equals(reader.getName())) {
524                    ret.tileWidth = Integer.parseInt(reader.getElementText());
525                }
526                if (new QName(WMTS_NS_URL, "MatrixHeight").equals(reader.getName())) {
527                    ret.matrixHeight = Integer.parseInt(reader.getElementText());
528                }
529                if (new QName(WMTS_NS_URL, "MatrixWidth").equals(reader.getName())) {
530                    ret.matrixWidth = Integer.parseInt(reader.getElementText());
531                }
532            }
533        }
534        if (ret.tileHeight != ret.tileWidth) {
535            throw new AssertionError(tr("Only square tiles are supported. {0}x{1} returned by server for TileMatrix identifier {2}",
536                    ret.tileHeight, ret.tileWidth, ret.identifier));
537        }
538        return ret;
539    }
540
541    /**
542     * Parses OperationMetadata section. Returns when reader is on OperationsMetadata closing tag.
543     * Sets this.baseUrl and this.transferMode
544     *
545     * @param reader StAX reader instance
546     * @throws XMLStreamException See {@link XMLStreamReader}
547     */
548    private void parseOperationMetadata(XMLStreamReader reader) throws XMLStreamException {
549        for (int event = reader.getEventType();
550                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT &&
551                        new QName(OWS_NS_URL, "OperationsMetadata").equals(reader.getName()));
552                event = reader.next()) {
553            if (event == XMLStreamReader.START_ELEMENT) {
554                if (new QName(OWS_NS_URL, "Operation").equals(reader.getName()) && "GetTile".equals(reader.getAttributeValue("", "name")) &&
555                        moveReaderToTag(reader, new QName[]{
556                                new QName(OWS_NS_URL, "DCP"),
557                                new QName(OWS_NS_URL, "HTTP"),
558                                new QName(OWS_NS_URL, "Get"),
559
560                        })) {
561                    this.baseUrl = reader.getAttributeValue(XLINK_NS_URL, "href");
562                    this.transferMode = getTransferMode(reader);
563                }
564            }
565        }
566    }
567
568    /**
569     * Parses Operation[@name='GetTile']/DCP/HTTP/Get section. Returns when reader is on Get closing tag.
570     * @param reader StAX reader instance
571     * @return TransferMode coded in this section
572     * @throws XMLStreamException See {@link XMLStreamReader}
573     */
574    private static TransferMode getTransferMode(XMLStreamReader reader) throws XMLStreamException {
575        QName getQname = new QName(OWS_NS_URL, "Get");
576
577        Utils.ensure(getQname.equals(reader.getName()), "WMTS Parser state invalid. Expected element %s, got %s",
578                getQname, reader.getName());
579        for (int event = reader.getEventType();
580                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && getQname.equals(reader.getName()));
581                event = reader.next()) {
582            if (event == XMLStreamReader.START_ELEMENT && new QName(OWS_NS_URL, "Constraint").equals(reader.getName())
583             && "GetEncoding".equals(reader.getAttributeValue("", "name"))) {
584                moveReaderToTag(reader, new QName[]{
585                        new QName(OWS_NS_URL, "AllowedValues"),
586                        new QName(OWS_NS_URL, "Value")
587                });
588                return TransferMode.fromString(reader.getElementText());
589            }
590        }
591        return null;
592    }
593
594    /**
595     * Moves reader to first occurrence of the structure equivalent of Xpath tags[0]/tags[1]../tags[n]. If fails to find
596     * moves the reader to the closing tag of current tag
597     *
598     * @param reader StAX reader instance
599     * @param tags array of tags
600     * @return true if tag was found, false otherwise
601     * @throws XMLStreamException See {@link XMLStreamReader}
602     */
603    private static boolean moveReaderToTag(XMLStreamReader reader, QName[] tags) throws XMLStreamException {
604        QName stopTag = reader.getName();
605        int currentLevel = 0;
606        QName searchTag = tags[currentLevel];
607        QName parentTag = null;
608        QName skipTag = null;
609
610        for (int event = 0; //skip current element, so we will not skip it as a whole
611                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && stopTag.equals(reader.getName()));
612                event = reader.next()) {
613            if (event == XMLStreamReader.END_ELEMENT && skipTag != null && skipTag.equals(reader.getName())) {
614                skipTag = null;
615            }
616            if (skipTag == null) {
617                if (event == XMLStreamReader.START_ELEMENT) {
618                    if (searchTag.equals(reader.getName())) {
619                        currentLevel += 1;
620                        if (currentLevel >= tags.length) {
621                            return true; // found!
622                        }
623                        parentTag = searchTag;
624                        searchTag = tags[currentLevel];
625                    } else {
626                        skipTag = reader.getName();
627                    }
628                }
629
630                if (event == XMLStreamReader.END_ELEMENT && parentTag != null && parentTag.equals(reader.getName())) {
631                    currentLevel -= 1;
632                    searchTag = parentTag;
633                    if (currentLevel >= 0) {
634                        parentTag = tags[currentLevel];
635                    } else {
636                        parentTag = null;
637                    }
638                }
639            }
640        }
641        return false;
642    }
643
644    private static String normalizeCapabilitiesUrl(String url) throws MalformedURLException {
645        URL inUrl = new URL(url);
646        URL ret = new URL(inUrl.getProtocol(), inUrl.getHost(), inUrl.getPort(), inUrl.getFile());
647        return ret.toExternalForm();
648    }
649
650    private static String crsToCode(String crsIdentifier) {
651        if (crsIdentifier.startsWith("urn:ogc:def:crs:")) {
652            return crsIdentifier.replaceFirst("urn:ogc:def:crs:([^:]*):.*:(.*)$", "$1:$2");
653        }
654        return crsIdentifier;
655    }
656
657    /**
658     * Initializes projection for this TileSource with projection
659     * @param proj projection to be used by this TileSource
660     */
661    public void initProjection(Projection proj) {
662        // getLayers will return only layers matching the name, if the user already choose the layer
663        // so we will not ask the user again to chose the layer, if he just changes projection
664        Collection<Layer> candidates = getLayers(currentLayer != null ? currentLayer.name : null, proj.toCode());
665        if (!candidates.isEmpty()) {
666            Layer newLayer = userSelectLayer(candidates);
667            if (newLayer != null) {
668                this.currentTileMatrixSet = newLayer.tileMatrixSet;
669                this.currentLayer = newLayer;
670                Collection<Double> scales = new ArrayList<>(currentTileMatrixSet.tileMatrix.size());
671                for (TileMatrix tileMatrix : currentTileMatrixSet.tileMatrix) {
672                    scales.add(tileMatrix.scaleDenominator * 0.28e-03);
673                }
674                this.nativeScaleList = new ScaleList(scales);
675            }
676        }
677        this.crsScale = getTileSize() * 0.28e-03 / proj.getMetersPerUnit();
678    }
679
680    /**
681     *
682     * @param name of the layer to match
683     * @param projectionCode projection code to match
684     * @return Collection of layers matching the name of the layer and projection, or only projection if name is not provided
685     */
686    private Collection<Layer> getLayers(String name, String projectionCode) {
687        Collection<Layer> ret = new ArrayList<>();
688        if (this.layers != null) {
689            for (Layer layer: this.layers) {
690                if ((name == null || name.equals(layer.name)) && (projectionCode == null || projectionCode.equals(layer.tileMatrixSet.crs))) {
691                    ret.add(layer);
692                }
693            }
694        }
695        return ret;
696    }
697
698    @Override
699    public int getTileSize() {
700        // no support for non-square tiles (tileHeight != tileWidth)
701        // and for different tile sizes at different zoom levels
702        Collection<Layer> layers = getLayers(null, Main.getProjection().toCode());
703        if (!layers.isEmpty()) {
704            return layers.iterator().next().tileMatrixSet.tileMatrix.get(0).tileHeight;
705        }
706        // if no layers is found, fallback to default mercator tile size. Maybe it will work
707        Main.warn("WMTS: Could not determine tile size. Using default tile size of: {0}", getDefaultTileSize());
708        return getDefaultTileSize();
709    }
710
711    @Override
712    public String getTileUrl(int zoom, int tilex, int tiley) {
713        String url;
714        if (currentLayer == null) {
715            return "";
716        }
717
718        if (currentLayer.baseUrl != null && transferMode == null) {
719            url = currentLayer.baseUrl;
720        } else {
721            switch (transferMode) {
722            case KVP:
723                url = baseUrl + URL_GET_ENCODING_PARAMS;
724                break;
725            case REST:
726                url = currentLayer.baseUrl;
727                break;
728            default:
729                url = "";
730                break;
731            }
732        }
733
734        TileMatrix tileMatrix = getTileMatrix(zoom);
735
736        if (tileMatrix == null) {
737            return ""; // no matrix, probably unsupported CRS selected.
738        }
739
740        return url.replaceAll("\\{layer\\}", this.currentLayer.name)
741                .replaceAll("\\{format\\}", this.currentLayer.format)
742                .replaceAll("\\{TileMatrixSet\\}", this.currentTileMatrixSet.identifier)
743                .replaceAll("\\{TileMatrix\\}", tileMatrix.identifier)
744                .replaceAll("\\{TileRow\\}", Integer.toString(tiley))
745                .replaceAll("\\{TileCol\\}", Integer.toString(tilex))
746                .replaceAll("(?i)\\{style\\}", this.currentLayer.style);
747    }
748
749    /**
750     *
751     * @param zoom zoom level
752     * @return TileMatrix that's working on this zoom level
753     */
754    private TileMatrix getTileMatrix(int zoom) {
755        if (zoom > getMaxZoom()) {
756            return null;
757        }
758        if (zoom < 0) {
759            return null;
760        }
761        return this.currentTileMatrixSet.tileMatrix.get(zoom);
762    }
763
764    @Override
765    public double getDistance(double lat1, double lon1, double lat2, double lon2) {
766        throw new UnsupportedOperationException("Not implemented");
767    }
768
769    @Override
770    public ICoordinate tileXYToLatLon(Tile tile) {
771        return tileXYToLatLon(tile.getXtile(), tile.getYtile(), tile.getZoom());
772    }
773
774    @Override
775    public ICoordinate tileXYToLatLon(TileXY xy, int zoom) {
776        return tileXYToLatLon(xy.getXIndex(), xy.getYIndex(), zoom);
777    }
778
779    @Override
780    public ICoordinate tileXYToLatLon(int x, int y, int zoom) {
781        TileMatrix matrix = getTileMatrix(zoom);
782        if (matrix == null) {
783            return Main.getProjection().getWorldBoundsLatLon().getCenter().toCoordinate();
784        }
785        double scale = matrix.scaleDenominator * this.crsScale;
786        EastNorth ret = new EastNorth(matrix.topLeftCorner.east() + x * scale, matrix.topLeftCorner.north() - y * scale);
787        return Main.getProjection().eastNorth2latlon(ret).toCoordinate();
788    }
789
790    @Override
791    public TileXY latLonToTileXY(double lat, double lon, int zoom) {
792        TileMatrix matrix = getTileMatrix(zoom);
793        if (matrix == null) {
794            return new TileXY(0, 0);
795        }
796
797        Projection proj = Main.getProjection();
798        EastNorth enPoint = proj.latlon2eastNorth(new LatLon(lat, lon));
799        double scale = matrix.scaleDenominator * this.crsScale;
800        return new TileXY(
801                (enPoint.east() - matrix.topLeftCorner.east()) / scale,
802                (matrix.topLeftCorner.north() - enPoint.north()) / scale
803                );
804    }
805
806    @Override
807    public TileXY latLonToTileXY(ICoordinate point, int zoom) {
808        return latLonToTileXY(point.getLat(),  point.getLon(), zoom);
809    }
810
811    @Override
812    public int getTileXMax(int zoom) {
813        return getTileXMax(zoom, Main.getProjection());
814    }
815
816    @Override
817    public int getTileXMin(int zoom) {
818        return 0;
819    }
820
821    @Override
822    public int getTileYMax(int zoom) {
823        return getTileYMax(zoom, Main.getProjection());
824    }
825
826    @Override
827    public int getTileYMin(int zoom) {
828        return 0;
829    }
830
831    @Override
832    public Point latLonToXY(double lat, double lon, int zoom) {
833        TileMatrix matrix = getTileMatrix(zoom);
834        if (matrix == null) {
835            return new Point(0, 0);
836        }
837        double scale = matrix.scaleDenominator * this.crsScale;
838        EastNorth point = Main.getProjection().latlon2eastNorth(new LatLon(lat, lon));
839        return new Point(
840                    (int) Math.round((point.east() - matrix.topLeftCorner.east())   / scale),
841                    (int) Math.round((matrix.topLeftCorner.north() - point.north()) / scale)
842                );
843    }
844
845    @Override
846    public Point latLonToXY(ICoordinate point, int zoom) {
847        return latLonToXY(point.getLat(), point.getLon(), zoom);
848    }
849
850    @Override
851    public Coordinate xyToLatLon(Point point, int zoom) {
852        return xyToLatLon(point.x, point.y, zoom);
853    }
854
855    @Override
856    public Coordinate xyToLatLon(int x, int y, int zoom) {
857        TileMatrix matrix = getTileMatrix(zoom);
858        if (matrix == null) {
859            return new Coordinate(0, 0);
860        }
861        double scale = matrix.scaleDenominator * this.crsScale;
862        Projection proj = Main.getProjection();
863        EastNorth ret = new EastNorth(
864                matrix.topLeftCorner.east() + x * scale,
865                matrix.topLeftCorner.north() - y * scale
866                );
867        LatLon ll = proj.eastNorth2latlon(ret);
868        return new Coordinate(ll.lat(), ll.lon());
869    }
870
871    @Override
872    public Map<String, String> getHeaders() {
873        return headers;
874    }
875
876    @Override
877    public int getMaxZoom() {
878        if (this.currentTileMatrixSet != null) {
879            return this.currentTileMatrixSet.tileMatrix.size()-1;
880        }
881        return 0;
882    }
883
884    @Override
885    public String getTileId(int zoom, int tilex, int tiley) {
886        return getTileUrl(zoom, tilex, tiley);
887    }
888
889    /**
890     * Checks if url is acceptable by this Tile Source
891     * @param url URL to check
892     */
893    public static void checkUrl(String url) {
894        CheckParameterUtil.ensureParameterNotNull(url, "url");
895        Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);
896        while (m.find()) {
897            boolean isSupportedPattern = false;
898            for (String pattern : ALL_PATTERNS) {
899                if (m.group().matches(pattern)) {
900                    isSupportedPattern = true;
901                    break;
902                }
903            }
904            if (!isSupportedPattern) {
905                throw new IllegalArgumentException(
906                        tr("{0} is not a valid WMS argument. Please check this server URL:\n{1}", m.group(), url));
907            }
908        }
909    }
910
911    /**
912     * @return set of projection codes that this TileSource supports
913     */
914    public Set<String> getSupportedProjections() {
915        Set<String> ret = new HashSet<>();
916        if (currentLayer == null) {
917            for (Layer layer: this.layers) {
918                ret.add(layer.tileMatrixSet.crs);
919            }
920        } else {
921            for (Layer layer: this.layers) {
922                if (currentLayer.name.equals(layer.name)) {
923                    ret.add(layer.tileMatrixSet.crs);
924                }
925            }
926        }
927        return ret;
928    }
929
930    private int getTileYMax(int zoom, Projection proj) {
931        TileMatrix matrix = getTileMatrix(zoom);
932        if (matrix == null) {
933            return 0;
934        }
935
936        if (matrix.matrixHeight != -1) {
937            return matrix.matrixHeight;
938        }
939
940        double scale = matrix.scaleDenominator * this.crsScale;
941        EastNorth min = matrix.topLeftCorner;
942        EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax());
943        return (int) Math.ceil(Math.abs(max.north() - min.north()) / scale);
944    }
945
946    private int getTileXMax(int zoom, Projection proj) {
947        TileMatrix matrix = getTileMatrix(zoom);
948        if (matrix == null) {
949            return 0;
950        }
951        if (matrix.matrixWidth != -1) {
952            return matrix.matrixWidth;
953        }
954
955        double scale = matrix.scaleDenominator * this.crsScale;
956        EastNorth min = matrix.topLeftCorner;
957        EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax());
958        return (int) Math.ceil(Math.abs(max.east() - min.east()) / scale);
959    }
960
961    /**
962     * Get native scales of tile source.
963     * @return {@link ScaleList} of native scales
964     */
965    public ScaleList getNativeScales() {
966        return nativeScaleList;
967    }
968
969}