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.nio.charset.StandardCharsets;
012import java.util.ArrayList;
013import java.util.Collection;
014import java.util.Collections;
015import java.util.HashSet;
016import java.util.List;
017import java.util.Map;
018import java.util.Map.Entry;
019import java.util.Set;
020import java.util.SortedSet;
021import java.util.Stack;
022import java.util.TreeSet;
023import java.util.concurrent.ConcurrentHashMap;
024import java.util.regex.Matcher;
025import java.util.regex.Pattern;
026import java.util.stream.Collectors;
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.XMLStreamException;
035import javax.xml.stream.XMLStreamReader;
036
037import org.openstreetmap.gui.jmapviewer.Coordinate;
038import org.openstreetmap.gui.jmapviewer.Tile;
039import org.openstreetmap.gui.jmapviewer.TileXY;
040import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
041import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
042import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
043import org.openstreetmap.josm.Main;
044import org.openstreetmap.josm.data.coor.EastNorth;
045import org.openstreetmap.josm.data.coor.LatLon;
046import org.openstreetmap.josm.data.projection.Projection;
047import org.openstreetmap.josm.data.projection.Projections;
048import org.openstreetmap.josm.gui.ExtendedDialog;
049import org.openstreetmap.josm.gui.layer.NativeScaleLayer.ScaleList;
050import org.openstreetmap.josm.io.CachedFile;
051import org.openstreetmap.josm.tools.CheckParameterUtil;
052import org.openstreetmap.josm.tools.GBC;
053import org.openstreetmap.josm.tools.Utils;
054
055/**
056 * Tile Source handling WMS providers
057 *
058 * @author Wiktor Niesiobędzki
059 * @since 8526
060 */
061public class WMTSTileSource extends AbstractTMSTileSource implements TemplatedTileSource {
062    /**
063     * WMTS namespace address
064     */
065    public static final String WMTS_NS_URL = "http://www.opengis.net/wmts/1.0";
066
067    // CHECKSTYLE.OFF: SingleSpaceSeparator
068    private static final QName QN_CONTENTS            = new QName(WMTSTileSource.WMTS_NS_URL, "Contents");
069    private static final QName QN_FORMAT              = new QName(WMTSTileSource.WMTS_NS_URL, "Format");
070    private static final QName QN_LAYER               = new QName(WMTSTileSource.WMTS_NS_URL, "Layer");
071    private static final QName QN_MATRIX_WIDTH        = new QName(WMTSTileSource.WMTS_NS_URL, "MatrixWidth");
072    private static final QName QN_MATRIX_HEIGHT       = new QName(WMTSTileSource.WMTS_NS_URL, "MatrixHeight");
073    private static final QName QN_RESOURCE_URL        = new QName(WMTSTileSource.WMTS_NS_URL, "ResourceURL");
074    private static final QName QN_SCALE_DENOMINATOR   = new QName(WMTSTileSource.WMTS_NS_URL, "ScaleDenominator");
075    private static final QName QN_STYLE               = new QName(WMTSTileSource.WMTS_NS_URL, "Style");
076    private static final QName QN_TILEMATRIX          = new QName(WMTSTileSource.WMTS_NS_URL, "TileMatrix");
077    private static final QName QN_TILEMATRIXSET       = new QName(WMTSTileSource.WMTS_NS_URL, "TileMatrixSet");
078    private static final QName QN_TILEMATRIX_SET_LINK = new QName(WMTSTileSource.WMTS_NS_URL, "TileMatrixSetLink");
079    private static final QName QN_TILE_WIDTH          = new QName(WMTSTileSource.WMTS_NS_URL, "TileWidth");
080    private static final QName QN_TILE_HEIGHT         = new QName(WMTSTileSource.WMTS_NS_URL, "TileHeight");
081    private static final QName QN_TOPLEFT_CORNER      = new QName(WMTSTileSource.WMTS_NS_URL, "TopLeftCorner");
082    // CHECKSTYLE.ON: SingleSpaceSeparator
083
084    private static final String PATTERN_HEADER = "\\{header\\(([^,]+),([^}]+)\\)\\}";
085
086    private static final String URL_GET_ENCODING_PARAMS = "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER={layer}&STYLE={style}&"
087            + "FORMAT={format}&tileMatrixSet={TileMatrixSet}&tileMatrix={TileMatrix}&tileRow={TileRow}&tileCol={TileCol}";
088
089    private static final String[] ALL_PATTERNS = {
090        PATTERN_HEADER,
091    };
092
093    private static class TileMatrix {
094        private String identifier;
095        private double scaleDenominator;
096        private EastNorth topLeftCorner;
097        private int tileWidth;
098        private int tileHeight;
099        private int matrixWidth = -1;
100        private int matrixHeight = -1;
101    }
102
103    private static class TileMatrixSetBuilder {
104        // sorted by zoom level
105        SortedSet<TileMatrix> tileMatrix = new TreeSet<>((o1, o2) -> -1 * Double.compare(o1.scaleDenominator, o2.scaleDenominator));
106        private String crs;
107        private String identifier;
108
109        TileMatrixSet build() {
110            return new TileMatrixSet(this);
111        }
112    }
113
114    private static class TileMatrixSet {
115
116        private final List<TileMatrix> tileMatrix;
117        private final String crs;
118        private final String identifier;
119
120        TileMatrixSet(TileMatrixSet tileMatrixSet) {
121            if (tileMatrixSet != null) {
122                tileMatrix = new ArrayList<>(tileMatrixSet.tileMatrix);
123                crs = tileMatrixSet.crs;
124                identifier = tileMatrixSet.identifier;
125            } else {
126                tileMatrix = Collections.emptyList();
127                crs = null;
128                identifier = null;
129            }
130        }
131
132        TileMatrixSet(TileMatrixSetBuilder builder) {
133            tileMatrix = new ArrayList<>(builder.tileMatrix);
134            crs = builder.crs;
135            identifier = builder.identifier;
136        }
137
138    }
139
140    private static class Layer {
141        private String format;
142        private String name;
143        private TileMatrixSet tileMatrixSet;
144        private String baseUrl;
145        private String style;
146        private final Collection<String> tileMatrixSetLinks = new ArrayList<>();
147
148        Layer(Layer l) {
149            if (l != null) {
150                format = l.format;
151                name = l.name;
152                baseUrl = l.baseUrl;
153                style = l.style;
154                tileMatrixSet = new TileMatrixSet(l.tileMatrixSet);
155            }
156        }
157
158        Layer() {
159        }
160    }
161
162    private static final class SelectLayerDialog extends ExtendedDialog {
163        private final transient List<Entry<String, List<Layer>>> layers;
164        private final JTable list;
165
166        SelectLayerDialog(Collection<Layer> layers) {
167            super(Main.parent, tr("Select WMTS layer"), new String[]{tr("Add layers"), tr("Cancel")});
168            this.layers = groupLayersByName(layers);
169            //getLayersTable(layers, Main.getProjection())
170            this.list = new JTable(
171                    new AbstractTableModel() {
172                        @Override
173                        public Object getValueAt(int rowIndex, int columnIndex) {
174                            switch (columnIndex) {
175                            case 0:
176                                return SelectLayerDialog.this.layers.get(rowIndex).getKey();
177                            case 1:
178                                return SelectLayerDialog.this.layers.get(rowIndex).getValue()
179                                        .stream()
180                                        .map(x -> x.tileMatrixSet.crs)
181                                        .collect(Collectors.joining(", "));
182                            case 2:
183                                return SelectLayerDialog.this.layers.get(rowIndex).getValue()
184                                        .stream()
185                                        .map(x -> x.tileMatrixSet.identifier)
186                                        .collect(Collectors.joining(", "));
187                            default:
188                                throw new IllegalArgumentException();
189                            }
190                        }
191
192                        @Override
193                        public int getRowCount() {
194                            return SelectLayerDialog.this.layers.size();
195                        }
196
197                        @Override
198                        public int getColumnCount() {
199                            return 3;
200                        }
201
202                        @Override
203                        public String getColumnName(int column) {
204                            switch (column) {
205                            case 0: return tr("Layer name");
206                            case 1: return tr("Projection");
207                            case 2: return tr("Matrix set identifier");
208                            default:
209                                throw new IllegalArgumentException();
210                            }
211                        }
212
213                        @Override
214                        public boolean isCellEditable(int row, int column) {
215                            return false;
216                        }
217                    });
218            this.list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
219            this.list.setRowSelectionAllowed(true);
220            this.list.setColumnSelectionAllowed(false);
221            JPanel panel = new JPanel(new GridBagLayout());
222            panel.add(new JScrollPane(this.list), GBC.eol().fill());
223            setContent(panel);
224        }
225
226        private static List<Entry<String, List<Layer>>> groupLayersByName(Collection<Layer> layers) {
227            Map<String, List<Layer>> layerByName = layers.stream().collect(Collectors.groupingBy(x -> x.name));
228            return layerByName.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList());
229        }
230
231        public String getSelectedLayer() {
232            int index = list.getSelectedRow();
233            if (index < 0) {
234                return null; //nothing selected
235            }
236            return layers.get(index).getKey();
237        }
238    }
239
240    private final Map<String, String> headers = new ConcurrentHashMap<>();
241    private final Collection<Layer> layers;
242    private Layer currentLayer;
243    private TileMatrixSet currentTileMatrixSet;
244    private double crsScale;
245    private GetCapabilitiesParseHelper.TransferMode transferMode;
246
247    private ScaleList nativeScaleList;
248
249    private final String defaultLayer;
250
251    /**
252     * Creates a tile source based on imagery info
253     * @param info imagery info
254     * @throws IOException if any I/O error occurs
255     * @throws IllegalArgumentException if any other error happens for the given imagery info
256     */
257    public WMTSTileSource(ImageryInfo info) throws IOException {
258        super(info);
259        CheckParameterUtil.ensureThat(info.getDefaultLayers().size() < 2, "At most 1 default layer for WMTS is supported");
260
261        this.baseUrl = GetCapabilitiesParseHelper.normalizeCapabilitiesUrl(handleTemplate(info.getUrl()));
262        this.layers = getCapabilities();
263        this.defaultLayer = info.getDefaultLayers().isEmpty() ? null : info.getDefaultLayers().iterator().next();
264        if (this.layers.isEmpty())
265            throw new IllegalArgumentException(tr("No layers defined by getCapabilities document: {0}", info.getUrl()));
266    }
267
268    /**
269     * Creates a dialog based on this tile source with all available layers and returns the name of selected layer
270     * @return Name of selected layer
271     */
272    public String userSelectLayer() {
273        Collection<String> layerNames = layers.stream().map(x -> x.name).collect(Collectors.toSet());
274
275        // if there is only one layer name no point in asking
276        if (layerNames.size() == 1)
277            return layerNames.iterator().next();
278
279        final SelectLayerDialog layerSelection = new SelectLayerDialog(layers);
280        if (layerSelection.showDialog().getValue() == 1) {
281            return layerSelection.getSelectedLayer();
282        }
283        return null;
284    }
285
286    private String handleTemplate(String url) {
287        Pattern pattern = Pattern.compile(PATTERN_HEADER);
288        StringBuffer output = new StringBuffer();
289        Matcher matcher = pattern.matcher(url);
290        while (matcher.find()) {
291            this.headers.put(matcher.group(1), matcher.group(2));
292            matcher.appendReplacement(output, "");
293        }
294        matcher.appendTail(output);
295        return output.toString();
296    }
297
298    /**
299     * @return capabilities
300     * @throws IOException in case of any I/O error
301     * @throws IllegalArgumentException in case of any other error
302     */
303    private Collection<Layer> getCapabilities() throws IOException {
304        try (CachedFile cf = new CachedFile(baseUrl); InputStream in = cf.setHttpHeaders(headers).
305                setMaxAge(7 * CachedFile.DAYS).
306                setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince).
307                getInputStream()) {
308            byte[] data = Utils.readBytesFromStream(in);
309            if (data == null || data.length == 0) {
310                cf.clear();
311                throw new IllegalArgumentException("Could not read data from: " + baseUrl);
312            }
313
314            try {
315                XMLStreamReader reader = GetCapabilitiesParseHelper.getReader(new ByteArrayInputStream(data));
316                Collection<Layer> ret = new ArrayList<>();
317                for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) {
318                    if (event == XMLStreamReader.START_ELEMENT) {
319                        if (GetCapabilitiesParseHelper.QN_OWS_OPERATIONS_METADATA.equals(reader.getName())) {
320                            parseOperationMetadata(reader);
321                        }
322
323                        if (QN_CONTENTS.equals(reader.getName())) {
324                            ret = parseContents(reader);
325                        }
326                    }
327                }
328                return ret;
329            } catch (XMLStreamException e) {
330                cf.clear();
331                Main.warn(new String(data, StandardCharsets.UTF_8));
332                throw new IllegalArgumentException(e);
333            }
334        }
335    }
336
337    /**
338     * Parse Contents tag. Returns when reader reaches Contents closing tag
339     *
340     * @param reader StAX reader instance
341     * @return collection of layers within contents with properly linked TileMatrixSets
342     * @throws XMLStreamException See {@link XMLStreamReader}
343     */
344    private static Collection<Layer> parseContents(XMLStreamReader reader) throws XMLStreamException {
345        Map<String, TileMatrixSet> matrixSetById = new ConcurrentHashMap<>();
346        Collection<Layer> layers = new ArrayList<>();
347        for (int event = reader.getEventType();
348                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_CONTENTS.equals(reader.getName()));
349                event = reader.next()) {
350            if (event == XMLStreamReader.START_ELEMENT) {
351                if (QN_LAYER.equals(reader.getName())) {
352                    layers.add(parseLayer(reader));
353                }
354                if (QN_TILEMATRIXSET.equals(reader.getName())) {
355                    TileMatrixSet entry = parseTileMatrixSet(reader);
356                    matrixSetById.put(entry.identifier, entry);
357                }
358            }
359        }
360        Collection<Layer> ret = new ArrayList<>();
361        // link layers to matrix sets
362        for (Layer l: layers) {
363            for (String tileMatrixId: l.tileMatrixSetLinks) {
364                Layer newLayer = new Layer(l); // create a new layer object for each tile matrix set supported
365                newLayer.tileMatrixSet = matrixSetById.get(tileMatrixId);
366                ret.add(newLayer);
367            }
368        }
369        return ret;
370    }
371
372    /**
373     * Parse Layer tag. Returns when reader will reach Layer closing tag
374     *
375     * @param reader StAX reader instance
376     * @return Layer object, with tileMatrixSetLinks and no tileMatrixSet attribute set.
377     * @throws XMLStreamException See {@link XMLStreamReader}
378     */
379    private static Layer parseLayer(XMLStreamReader reader) throws XMLStreamException {
380        Layer layer = new Layer();
381        Stack<QName> tagStack = new Stack<>();
382
383        for (int event = reader.getEventType();
384                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_LAYER.equals(reader.getName()));
385                event = reader.next()) {
386            if (event == XMLStreamReader.START_ELEMENT) {
387                tagStack.push(reader.getName());
388                if (tagStack.size() == 2) {
389                    if (QN_FORMAT.equals(reader.getName())) {
390                        layer.format = reader.getElementText();
391                    } else if (GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER.equals(reader.getName())) {
392                        layer.name = reader.getElementText();
393                    } else if (QN_RESOURCE_URL.equals(reader.getName()) &&
394                            "tile".equals(reader.getAttributeValue("", "resourceType"))) {
395                        layer.baseUrl = reader.getAttributeValue("", "template");
396                    } else if (QN_STYLE.equals(reader.getName()) &&
397                            "true".equals(reader.getAttributeValue("", "isDefault"))) {
398                        if (GetCapabilitiesParseHelper.moveReaderToTag(reader, new QName[] {GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER})) {
399                            layer.style = reader.getElementText();
400                            tagStack.push(reader.getName()); // keep tagStack in sync
401                        }
402                    } else if (QN_TILEMATRIX_SET_LINK.equals(reader.getName())) {
403                        layer.tileMatrixSetLinks.add(praseTileMatrixSetLink(reader));
404                    } else {
405                        GetCapabilitiesParseHelper.moveReaderToEndCurrentTag(reader);
406                    }
407                }
408            }
409            // need to get event type from reader, as parsing might have change position of reader
410            if (reader.getEventType() == XMLStreamReader.END_ELEMENT) {
411                QName start = tagStack.pop();
412                if (!start.equals(reader.getName())) {
413                    throw new IllegalStateException(tr("WMTS Parser error - start element {0} has different name than end element {2}",
414                            start, reader.getName()));
415                }
416            }
417        }
418        if (layer.style == null) {
419            layer.style = "";
420        }
421        return layer;
422    }
423
424    /**
425     * Gets TileMatrixSetLink value. Returns when reader is on TileMatrixSetLink closing tag
426     *
427     * @param reader StAX reader instance
428     * @return TileMatrixSetLink identifier
429     * @throws XMLStreamException See {@link XMLStreamReader}
430     */
431    private static String praseTileMatrixSetLink(XMLStreamReader reader) throws XMLStreamException {
432        String ret = null;
433        for (int event = reader.getEventType();
434                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT &&
435                        QN_TILEMATRIX_SET_LINK.equals(reader.getName()));
436                event = reader.next()) {
437            if (event == XMLStreamReader.START_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName())) {
438                ret = reader.getElementText();
439            }
440        }
441        return ret;
442    }
443
444    /**
445     * Parses TileMatrixSet section. Returns when reader is on TileMatrixSet closing tag
446     * @param reader StAX reader instance
447     * @return TileMatrixSet object
448     * @throws XMLStreamException See {@link XMLStreamReader}
449     */
450    private static TileMatrixSet parseTileMatrixSet(XMLStreamReader reader) throws XMLStreamException {
451        TileMatrixSetBuilder matrixSet = new TileMatrixSetBuilder();
452        for (int event = reader.getEventType();
453                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName()));
454                event = reader.next()) {
455                    if (event == XMLStreamReader.START_ELEMENT) {
456                        if (GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER.equals(reader.getName())) {
457                            matrixSet.identifier = reader.getElementText();
458                        }
459                        if (GetCapabilitiesParseHelper.QN_OWS_SUPPORTED_CRS.equals(reader.getName())) {
460                            matrixSet.crs = GetCapabilitiesParseHelper.crsToCode(reader.getElementText());
461                        }
462                        if (QN_TILEMATRIX.equals(reader.getName())) {
463                            matrixSet.tileMatrix.add(parseTileMatrix(reader, matrixSet.crs));
464                        }
465                    }
466        }
467        return matrixSet.build();
468    }
469
470    /**
471     * Parses TileMatrix section. Returns when reader is on TileMatrix closing tag.
472     * @param reader StAX reader instance
473     * @param matrixCrs projection used by this matrix
474     * @return TileMatrix object
475     * @throws XMLStreamException See {@link XMLStreamReader}
476     */
477    private static TileMatrix parseTileMatrix(XMLStreamReader reader, String matrixCrs) throws XMLStreamException {
478        Projection matrixProj = Projections.getProjectionByCode(matrixCrs);
479        TileMatrix ret = new TileMatrix();
480
481        if (matrixProj == null) {
482            // use current projection if none found. Maybe user is using custom string
483            matrixProj = Main.getProjection();
484        }
485        for (int event = reader.getEventType();
486                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_TILEMATRIX.equals(reader.getName()));
487                event = reader.next()) {
488            if (event == XMLStreamReader.START_ELEMENT) {
489                if (GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER.equals(reader.getName())) {
490                    ret.identifier = reader.getElementText();
491                }
492                if (QN_SCALE_DENOMINATOR.equals(reader.getName())) {
493                    ret.scaleDenominator = Double.parseDouble(reader.getElementText());
494                }
495                if (QN_TOPLEFT_CORNER.equals(reader.getName())) {
496                    String[] topLeftCorner = reader.getElementText().split(" ");
497                    if (matrixProj.switchXY()) {
498                        ret.topLeftCorner = new EastNorth(Double.parseDouble(topLeftCorner[1]), Double.parseDouble(topLeftCorner[0]));
499                    } else {
500                        ret.topLeftCorner = new EastNorth(Double.parseDouble(topLeftCorner[0]), Double.parseDouble(topLeftCorner[1]));
501                    }
502                }
503                if (QN_TILE_HEIGHT.equals(reader.getName())) {
504                    ret.tileHeight = Integer.parseInt(reader.getElementText());
505                }
506                if (QN_TILE_WIDTH.equals(reader.getName())) {
507                    ret.tileWidth = Integer.parseInt(reader.getElementText());
508                }
509                if (QN_MATRIX_HEIGHT.equals(reader.getName())) {
510                    ret.matrixHeight = Integer.parseInt(reader.getElementText());
511                }
512                if (QN_MATRIX_WIDTH.equals(reader.getName())) {
513                    ret.matrixWidth = Integer.parseInt(reader.getElementText());
514                }
515            }
516        }
517        if (ret.tileHeight != ret.tileWidth) {
518            throw new AssertionError(tr("Only square tiles are supported. {0}x{1} returned by server for TileMatrix identifier {2}",
519                    ret.tileHeight, ret.tileWidth, ret.identifier));
520        }
521        return ret;
522    }
523
524    /**
525     * Parses OperationMetadata section. Returns when reader is on OperationsMetadata closing tag.
526     * Sets this.baseUrl and this.transferMode
527     *
528     * @param reader StAX reader instance
529     * @throws XMLStreamException See {@link XMLStreamReader}
530     */
531    private void parseOperationMetadata(XMLStreamReader reader) throws XMLStreamException {
532        for (int event = reader.getEventType();
533                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT &&
534                        GetCapabilitiesParseHelper.QN_OWS_OPERATIONS_METADATA.equals(reader.getName()));
535                event = reader.next()) {
536            if (event == XMLStreamReader.START_ELEMENT &&
537                    GetCapabilitiesParseHelper.QN_OWS_OPERATION.equals(reader.getName()) &&
538                    "GetTile".equals(reader.getAttributeValue("", "name")) &&
539                    GetCapabilitiesParseHelper.moveReaderToTag(reader, new QName[] {
540                            GetCapabilitiesParseHelper.QN_OWS_DCP,
541                            GetCapabilitiesParseHelper.QN_OWS_HTTP,
542                            GetCapabilitiesParseHelper.QN_OWS_GET,
543                    })) {
544                this.baseUrl = reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href");
545                this.transferMode = GetCapabilitiesParseHelper.getTransferMode(reader);
546            }
547        }
548    }
549
550    /**
551     * Initializes projection for this TileSource with projection
552     * @param proj projection to be used by this TileSource
553     */
554    public void initProjection(Projection proj) {
555        // getLayers will return only layers matching the name, if the user already choose the layer
556        // so we will not ask the user again to chose the layer, if he just changes projection
557        Collection<Layer> candidates = getLayers(currentLayer != null ? currentLayer.name : defaultLayer, proj.toCode());
558        if (candidates.size() == 1) {
559
560            Layer newLayer = candidates.iterator().next();
561            if (newLayer != null) {
562                this.currentTileMatrixSet = newLayer.tileMatrixSet;
563                this.currentLayer = newLayer;
564                Collection<Double> scales = new ArrayList<>(currentTileMatrixSet.tileMatrix.size());
565                for (TileMatrix tileMatrix : currentTileMatrixSet.tileMatrix) {
566                    scales.add(tileMatrix.scaleDenominator * 0.28e-03);
567                }
568                this.nativeScaleList = new ScaleList(scales);
569            }
570        } else if (candidates.size() > 1) {
571            Main.warn("More than one layer WMTS available: {0} for projection {1} and name {2}. Do not know which to process",
572                    candidates.stream().map(x -> x.name + ": " + x.tileMatrixSet.identifier).collect(Collectors.joining(", ")),
573                    proj.toCode(),
574                    currentLayer != null ? currentLayer.name : defaultLayer
575                    );
576        }
577        this.crsScale = getTileSize() * 0.28e-03 / proj.getMetersPerUnit();
578    }
579
580    /**
581     *
582     * @param name of the layer to match
583     * @param projectionCode projection code to match
584     * @return Collection of layers matching the name of the layer and projection, or only projection if name is not provided
585     */
586    private Collection<Layer> getLayers(String name, String projectionCode) {
587        Collection<Layer> ret = new ArrayList<>();
588        if (this.layers != null) {
589            for (Layer layer: this.layers) {
590                if ((name == null || name.equals(layer.name)) && (projectionCode == null || projectionCode.equals(layer.tileMatrixSet.crs))) {
591                    ret.add(layer);
592                }
593            }
594        }
595        return ret;
596    }
597
598    @Override
599    public int getTileSize() {
600        // no support for non-square tiles (tileHeight != tileWidth)
601        // and for different tile sizes at different zoom levels
602        Collection<Layer> projLayers = getLayers(null, Main.getProjection().toCode());
603        if (!projLayers.isEmpty()) {
604            return projLayers.iterator().next().tileMatrixSet.tileMatrix.get(0).tileHeight;
605        }
606        // if no layers is found, fallback to default mercator tile size. Maybe it will work
607        Main.warn("WMTS: Could not determine tile size. Using default tile size of: {0}", getDefaultTileSize());
608        return getDefaultTileSize();
609    }
610
611    @Override
612    public String getTileUrl(int zoom, int tilex, int tiley) {
613        if (currentLayer == null) {
614            return "";
615        }
616
617        String url;
618        if (currentLayer.baseUrl != null && transferMode == null) {
619            url = currentLayer.baseUrl;
620        } else {
621            switch (transferMode) {
622            case KVP:
623                url = baseUrl + URL_GET_ENCODING_PARAMS;
624                break;
625            case REST:
626                url = currentLayer.baseUrl;
627                break;
628            default:
629                url = "";
630                break;
631            }
632        }
633
634        TileMatrix tileMatrix = getTileMatrix(zoom);
635
636        if (tileMatrix == null) {
637            return ""; // no matrix, probably unsupported CRS selected.
638        }
639
640        return url.replaceAll("\\{layer\\}", this.currentLayer.name)
641                .replaceAll("\\{format\\}", this.currentLayer.format)
642                .replaceAll("\\{TileMatrixSet\\}", this.currentTileMatrixSet.identifier)
643                .replaceAll("\\{TileMatrix\\}", tileMatrix.identifier)
644                .replaceAll("\\{TileRow\\}", Integer.toString(tiley))
645                .replaceAll("\\{TileCol\\}", Integer.toString(tilex))
646                .replaceAll("(?i)\\{style\\}", this.currentLayer.style);
647    }
648
649    /**
650     *
651     * @param zoom zoom level
652     * @return TileMatrix that's working on this zoom level
653     */
654    private TileMatrix getTileMatrix(int zoom) {
655        if (zoom > getMaxZoom()) {
656            return null;
657        }
658        if (zoom < 0) {
659            return null;
660        }
661        return this.currentTileMatrixSet.tileMatrix.get(zoom);
662    }
663
664    @Override
665    public double getDistance(double lat1, double lon1, double lat2, double lon2) {
666        throw new UnsupportedOperationException("Not implemented");
667    }
668
669    @Override
670    public ICoordinate tileXYToLatLon(Tile tile) {
671        return tileXYToLatLon(tile.getXtile(), tile.getYtile(), tile.getZoom());
672    }
673
674    @Override
675    public ICoordinate tileXYToLatLon(TileXY xy, int zoom) {
676        return tileXYToLatLon(xy.getXIndex(), xy.getYIndex(), zoom);
677    }
678
679    @Override
680    public ICoordinate tileXYToLatLon(int x, int y, int zoom) {
681        TileMatrix matrix = getTileMatrix(zoom);
682        if (matrix == null) {
683            return Main.getProjection().getWorldBoundsLatLon().getCenter().toCoordinate();
684        }
685        double scale = matrix.scaleDenominator * this.crsScale;
686        EastNorth ret = new EastNorth(matrix.topLeftCorner.east() + x * scale, matrix.topLeftCorner.north() - y * scale);
687        return Main.getProjection().eastNorth2latlon(ret).toCoordinate();
688    }
689
690    @Override
691    public TileXY latLonToTileXY(double lat, double lon, int zoom) {
692        TileMatrix matrix = getTileMatrix(zoom);
693        if (matrix == null) {
694            return new TileXY(0, 0);
695        }
696
697        Projection proj = Main.getProjection();
698        EastNorth enPoint = proj.latlon2eastNorth(new LatLon(lat, lon));
699        double scale = matrix.scaleDenominator * this.crsScale;
700        return new TileXY(
701                (enPoint.east() - matrix.topLeftCorner.east()) / scale,
702                (matrix.topLeftCorner.north() - enPoint.north()) / scale
703                );
704    }
705
706    @Override
707    public TileXY latLonToTileXY(ICoordinate point, int zoom) {
708        return latLonToTileXY(point.getLat(), point.getLon(), zoom);
709    }
710
711    @Override
712    public int getTileXMax(int zoom) {
713        return getTileXMax(zoom, Main.getProjection());
714    }
715
716    @Override
717    public int getTileXMin(int zoom) {
718        return 0;
719    }
720
721    @Override
722    public int getTileYMax(int zoom) {
723        return getTileYMax(zoom, Main.getProjection());
724    }
725
726    @Override
727    public int getTileYMin(int zoom) {
728        return 0;
729    }
730
731    @Override
732    public Point latLonToXY(double lat, double lon, int zoom) {
733        TileMatrix matrix = getTileMatrix(zoom);
734        if (matrix == null) {
735            return new Point(0, 0);
736        }
737        double scale = matrix.scaleDenominator * this.crsScale;
738        EastNorth point = Main.getProjection().latlon2eastNorth(new LatLon(lat, lon));
739        return new Point(
740                    (int) Math.round((point.east() - matrix.topLeftCorner.east()) / scale),
741                    (int) Math.round((matrix.topLeftCorner.north() - point.north()) / scale)
742                );
743    }
744
745    @Override
746    public Point latLonToXY(ICoordinate point, int zoom) {
747        return latLonToXY(point.getLat(), point.getLon(), zoom);
748    }
749
750    @Override
751    public Coordinate xyToLatLon(Point point, int zoom) {
752        return xyToLatLon(point.x, point.y, zoom);
753    }
754
755    @Override
756    public Coordinate xyToLatLon(int x, int y, int zoom) {
757        TileMatrix matrix = getTileMatrix(zoom);
758        if (matrix == null) {
759            return new Coordinate(0, 0);
760        }
761        double scale = matrix.scaleDenominator * this.crsScale;
762        Projection proj = Main.getProjection();
763        EastNorth ret = new EastNorth(
764                matrix.topLeftCorner.east() + x * scale,
765                matrix.topLeftCorner.north() - y * scale
766                );
767        LatLon ll = proj.eastNorth2latlon(ret);
768        return new Coordinate(ll.lat(), ll.lon());
769    }
770
771    @Override
772    public Map<String, String> getHeaders() {
773        return headers;
774    }
775
776    @Override
777    public int getMaxZoom() {
778        if (this.currentTileMatrixSet != null) {
779            return this.currentTileMatrixSet.tileMatrix.size()-1;
780        }
781        return 0;
782    }
783
784    @Override
785    public String getTileId(int zoom, int tilex, int tiley) {
786        return getTileUrl(zoom, tilex, tiley);
787    }
788
789    /**
790     * Checks if url is acceptable by this Tile Source
791     * @param url URL to check
792     */
793    public static void checkUrl(String url) {
794        CheckParameterUtil.ensureParameterNotNull(url, "url");
795        Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);
796        while (m.find()) {
797            boolean isSupportedPattern = false;
798            for (String pattern : ALL_PATTERNS) {
799                if (m.group().matches(pattern)) {
800                    isSupportedPattern = true;
801                    break;
802                }
803            }
804            if (!isSupportedPattern) {
805                throw new IllegalArgumentException(
806                        tr("{0} is not a valid WMS argument. Please check this server URL:\n{1}", m.group(), url));
807            }
808        }
809    }
810
811    /**
812     * @return set of projection codes that this TileSource supports
813     */
814    public Set<String> getSupportedProjections() {
815        Set<String> ret = new HashSet<>();
816        if (currentLayer == null) {
817            for (Layer layer: this.layers) {
818                ret.add(layer.tileMatrixSet.crs);
819            }
820        } else {
821            for (Layer layer: this.layers) {
822                if (currentLayer.name.equals(layer.name)) {
823                    ret.add(layer.tileMatrixSet.crs);
824                }
825            }
826        }
827        return ret;
828    }
829
830    private int getTileYMax(int zoom, Projection proj) {
831        TileMatrix matrix = getTileMatrix(zoom);
832        if (matrix == null) {
833            return 0;
834        }
835
836        if (matrix.matrixHeight != -1) {
837            return matrix.matrixHeight;
838        }
839
840        double scale = matrix.scaleDenominator * this.crsScale;
841        EastNorth min = matrix.topLeftCorner;
842        EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax());
843        return (int) Math.ceil(Math.abs(max.north() - min.north()) / scale);
844    }
845
846    private int getTileXMax(int zoom, Projection proj) {
847        TileMatrix matrix = getTileMatrix(zoom);
848        if (matrix == null) {
849            return 0;
850        }
851        if (matrix.matrixWidth != -1) {
852            return matrix.matrixWidth;
853        }
854
855        double scale = matrix.scaleDenominator * this.crsScale;
856        EastNorth min = matrix.topLeftCorner;
857        EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax());
858        return (int) Math.ceil(Math.abs(max.east() - min.east()) / scale);
859    }
860
861    /**
862     * Get native scales of tile source.
863     * @return {@link ScaleList} of native scales
864     */
865    public ScaleList getNativeScales() {
866        return nativeScaleList;
867    }
868
869}