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