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.Dimension;
007import java.awt.GridBagLayout;
008import java.awt.Point;
009import java.io.ByteArrayInputStream;
010import java.io.IOException;
011import java.io.InputStream;
012import java.net.MalformedURLException;
013import java.net.URL;
014import java.util.ArrayList;
015import java.util.Collection;
016import java.util.Comparator;
017import java.util.HashSet;
018import java.util.Map;
019import java.util.Set;
020import java.util.SortedSet;
021import java.util.TreeSet;
022import java.util.concurrent.ConcurrentHashMap;
023import java.util.regex.Matcher;
024import java.util.regex.Pattern;
025
026import javax.swing.JPanel;
027import javax.swing.JTable;
028import javax.swing.ListSelectionModel;
029import javax.swing.table.AbstractTableModel;
030import javax.xml.XMLConstants;
031import javax.xml.namespace.QName;
032import javax.xml.parsers.DocumentBuilder;
033import javax.xml.parsers.DocumentBuilderFactory;
034import javax.xml.parsers.ParserConfigurationException;
035import javax.xml.xpath.XPath;
036import javax.xml.xpath.XPathConstants;
037import javax.xml.xpath.XPathExpression;
038import javax.xml.xpath.XPathExpressionException;
039import javax.xml.xpath.XPathFactory;
040
041import org.openstreetmap.gui.jmapviewer.Coordinate;
042import org.openstreetmap.gui.jmapviewer.Tile;
043import org.openstreetmap.gui.jmapviewer.TileXY;
044import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
045import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
046import org.openstreetmap.gui.jmapviewer.tilesources.TMSTileSource;
047import org.openstreetmap.josm.Main;
048import org.openstreetmap.josm.data.coor.EastNorth;
049import org.openstreetmap.josm.data.coor.LatLon;
050import org.openstreetmap.josm.data.projection.Projection;
051import org.openstreetmap.josm.data.projection.Projections;
052import org.openstreetmap.josm.gui.ExtendedDialog;
053import org.openstreetmap.josm.io.CachedFile;
054import org.openstreetmap.josm.tools.CheckParameterUtil;
055import org.openstreetmap.josm.tools.GBC;
056import org.openstreetmap.josm.tools.Utils;
057import org.w3c.dom.Document;
058import org.w3c.dom.Node;
059import org.w3c.dom.NodeList;
060
061/**
062 * Tile Source handling WMS providers
063 *
064 * @author Wiktor Niesiobędzki
065 * @since 8526
066 */
067public class WMTSTileSource extends TMSTileSource implements TemplatedTileSource {
068    private static final String PATTERN_HEADER  = "\\{header\\(([^,]+),([^}]+)\\)\\}";
069
070    private static final String URL_GET_ENCODING_PARAMS = "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER={layer}&STYLE={Style}&"
071            + "FORMAT={format}&tileMatrixSet={TileMatrixSet}&tileMatrix={TileMatrix}&tileRow={TileRow}&tileCol={TileCol}";
072
073    private static final String[] ALL_PATTERNS = {
074        PATTERN_HEADER,
075    };
076
077    private static class TileMatrix {
078        private String identifier;
079        private double scaleDenominator;
080        private EastNorth topLeftCorner;
081        private int tileWidth;
082        private int tileHeight;
083        private int matrixWidth = -1;
084        private int matrixHeight = -1;
085    }
086
087    private static class TileMatrixSet {
088        SortedSet<TileMatrix> tileMatrix = new TreeSet<>(new Comparator<TileMatrix>() {
089            @Override
090            public int compare(TileMatrix o1, TileMatrix o2) {
091                // reverse the order, so it will be from greatest (lowest zoom level) to lowest value (highest zoom level)
092                return -1 * Double.compare(o1.scaleDenominator, o2.scaleDenominator);
093            }
094        }); // sorted by zoom level
095        private String crs;
096        private String identifier;
097    }
098
099    private static class Layer {
100        private String format;
101        private String name;
102        private TileMatrixSet tileMatrixSet;
103        private String baseUrl;
104        private String style;
105    }
106
107    private enum TransferMode {
108        KVP("KVP"),
109        REST("RESTful");
110
111        private final String typeString;
112
113        TransferMode(String urlString) {
114            this.typeString = urlString;
115        }
116
117        private String getTypeString() {
118            return typeString;
119        }
120
121        private static TransferMode fromString(String s) {
122            for (TransferMode type : TransferMode.values()) {
123                if (type.getTypeString().equals(s)) {
124                    return type;
125                }
126            }
127            return null;
128        }
129    }
130
131    private static final class SelectLayerDialog extends ExtendedDialog {
132        private final Layer[] layers;
133        private final JTable list;
134
135        SelectLayerDialog(Collection<Layer> layers) {
136            super(Main.parent, tr("Select WMTS layer"), new String[]{tr("Add layers"), tr("Cancel")});
137            this.layers = layers.toArray(new Layer[]{});
138            //getLayersTable(layers, Main.getProjection())
139            this.list = new JTable(
140                    new AbstractTableModel() {
141                        @Override
142                        public Object getValueAt(int rowIndex, int columnIndex) {
143                            switch (columnIndex) {
144                            case 0:
145                                return SelectLayerDialog.this.layers[rowIndex].name;
146                            case 1:
147                                return SelectLayerDialog.this.layers[rowIndex].tileMatrixSet.crs;
148                            case 2:
149                                return SelectLayerDialog.this.layers[rowIndex].tileMatrixSet.identifier;
150                            default:
151                                throw new IllegalArgumentException();
152                            }
153                        }
154
155                        @Override
156                        public int getRowCount() {
157                            return SelectLayerDialog.this.layers.length;
158                        }
159
160                        @Override
161                        public int getColumnCount() {
162                            return 3;
163                        }
164
165                        @Override
166                        public String getColumnName(int column) {
167                            switch (column) {
168                            case 0: return tr("Layer name");
169                            case 1: return tr("Projection");
170                            case 2: return tr("Matrix set identifier");
171                            default:
172                                throw new IllegalArgumentException();
173                            }
174                        }
175
176                        @Override
177                        public boolean isCellEditable(int row, int column) {
178                            return false;
179                        }
180                    });
181            this.list.setPreferredSize(new Dimension(400, 400));
182            this.list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
183            this.list.setRowSelectionAllowed(true);
184            this.list.setColumnSelectionAllowed(false);
185            JPanel panel = new JPanel(new GridBagLayout());
186            panel.add(this.list, GBC.eol().fill());
187            setContent(panel);
188        }
189
190        public Layer getSelectedLayer() {
191            int index = list.getSelectedRow();
192            if (index < 0) {
193                return null; //nothing selected
194            }
195            return layers[index];
196        }
197    }
198
199    private final Map<String, String> headers = new ConcurrentHashMap<>();
200    private Collection<Layer> layers;
201    private Layer currentLayer;
202    private TileMatrixSet currentTileMatrixSet;
203    private double crsScale;
204    private TransferMode transferMode;
205
206    /**
207     * Creates a tile source based on imagery info
208     * @param info imagery info
209     * @throws IOException if any I/O error occurs
210     */
211    public WMTSTileSource(ImageryInfo info) throws IOException {
212        super(info);
213        this.baseUrl = normalizeCapabilitiesUrl(handleTemplate(info.getUrl()));
214        this.layers = getCapabilities();
215        if (this.layers.isEmpty())
216            throw new IllegalArgumentException(tr("No layers defined by getCapabilities document: {0}", info.getUrl()));
217    }
218
219    private Layer userSelectLayer(Collection<Layer> layers) {
220        if (layers.size() == 1)
221            return layers.iterator().next();
222        Layer ret = null;
223
224        final SelectLayerDialog layerSelection = new SelectLayerDialog(layers);
225        if (layerSelection.showDialog().getValue() == 1) {
226            ret = layerSelection.getSelectedLayer();
227            // TODO: save layer information into ImageryInfo / ImageryPreferences?
228        }
229        if (ret == null) {
230            // user canceled operation or did not choose any layer
231            throw new IllegalArgumentException(tr("No layer selected"));
232        }
233        return ret;
234    }
235
236    private String handleTemplate(String url) {
237        Pattern pattern = Pattern.compile(PATTERN_HEADER);
238        StringBuffer output = new StringBuffer();
239        Matcher matcher = pattern.matcher(url);
240        while (matcher.find()) {
241            this.headers.put(matcher.group(1), matcher.group(2));
242            matcher.appendReplacement(output, "");
243        }
244        matcher.appendTail(output);
245        return output.toString();
246    }
247
248    private Collection<Layer> getCapabilities() throws IOException {
249        DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
250        builderFactory.setValidating(false);
251        builderFactory.setNamespaceAware(false);
252        try {
253            builderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
254        } catch (ParserConfigurationException e) {
255            //this should not happen
256            throw new IllegalArgumentException(e);
257        }
258        DocumentBuilder builder = null;
259        InputStream in = new CachedFile(baseUrl).
260                setHttpHeaders(headers).
261                setMaxAge(7 * CachedFile.DAYS).
262                setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince).
263                getInputStream();
264        try {
265            builder = builderFactory.newDocumentBuilder();
266            byte[] data = Utils.readBytesFromStream(in);
267            if (data == null || data.length == 0) {
268                throw new IllegalArgumentException("Could not read data from: " + baseUrl);
269            }
270            Document document = builder.parse(new ByteArrayInputStream(data));
271            Node getTileOperation = getByXpath(document,
272                    "/Capabilities/OperationsMetadata/Operation[@name=\"GetTile\"]/DCP/HTTP/Get").item(0);
273            this.baseUrl = getStringByXpath(getTileOperation, "@href");
274            this.transferMode = TransferMode.fromString(getStringByXpath(getTileOperation,
275                    "Constraint[@name=\"GetEncoding\"]/AllowedValues/Value"));
276            NodeList layersNodeList = getByXpath(document, "/Capabilities/Contents/Layer");
277            Map<String, TileMatrixSet> matrixSetById = parseMatrices(getByXpath(document, "/Capabilities/Contents/TileMatrixSet"));
278            return parseLayer(layersNodeList, matrixSetById);
279        } catch (Exception e) {
280            throw new IllegalArgumentException(e);
281        }
282    }
283
284    private static String normalizeCapabilitiesUrl(String url) throws MalformedURLException {
285        URL inUrl = new URL(url);
286        URL ret = new URL(inUrl.getProtocol(), inUrl.getHost(), inUrl.getPort(), inUrl.getFile());
287        return ret.toExternalForm();
288    }
289
290    private Collection<Layer> parseLayer(NodeList nodeList, Map<String, TileMatrixSet> matrixSetById) throws XPathExpressionException {
291        Collection<Layer> ret = new ArrayList<>();
292        for (int layerId = 0; layerId < nodeList.getLength(); layerId++) {
293            Node layerNode = nodeList.item(layerId);
294            NodeList tileMatrixSetLinks = getByXpath(layerNode, "TileMatrixSetLink");
295
296            // we add an layer for all matrix sets to allow user to choose, with which tileset he wants to work
297            for (int tileMatrixId = 0; tileMatrixId < tileMatrixSetLinks.getLength(); tileMatrixId++) {
298                Layer layer = new Layer();
299                layer.format = getStringByXpath(layerNode, "Format");
300                layer.name = getStringByXpath(layerNode, "Identifier");
301                layer.baseUrl = getStringByXpath(layerNode, "ResourceURL[@resourceType='tile']/@template");
302                layer.style = getStringByXpath(layerNode, "Style[@isDefault='true']/Identifier");
303                if (layer.style == null) {
304                    layer.style = "";
305                }
306                Node tileMatrixLink = tileMatrixSetLinks.item(tileMatrixId);
307                TileMatrixSet tms = matrixSetById.get(getStringByXpath(tileMatrixLink, "TileMatrixSet"));
308                layer.tileMatrixSet = tms;
309                ret.add(layer);
310            }
311        }
312        return ret;
313
314    }
315
316    private Map<String, TileMatrixSet> parseMatrices(NodeList nodeList) throws XPathExpressionException {
317        Map<String, TileMatrixSet> ret = new ConcurrentHashMap<>();
318        for (int matrixSetId = 0; matrixSetId < nodeList.getLength(); matrixSetId++) {
319            Node matrixSetNode = nodeList.item(matrixSetId);
320            TileMatrixSet matrixSet = new TileMatrixSet();
321            matrixSet.identifier = getStringByXpath(matrixSetNode, "Identifier");
322            matrixSet.crs = crsToCode(getStringByXpath(matrixSetNode, "SupportedCRS"));
323            NodeList tileMatrixList = getByXpath(matrixSetNode, "TileMatrix");
324            Projection matrixProj = Projections.getProjectionByCode(matrixSet.crs);
325            if (matrixProj == null) {
326                // use current projection if none found. Maybe user is using custom string
327                matrixProj = Main.getProjection();
328            }
329            for (int matrixId = 0; matrixId < tileMatrixList.getLength(); matrixId++) {
330                Node tileMatrixNode = tileMatrixList.item(matrixId);
331                TileMatrix tileMatrix = new TileMatrix();
332                tileMatrix.identifier = getStringByXpath(tileMatrixNode, "Identifier");
333                tileMatrix.scaleDenominator = Double.parseDouble(getStringByXpath(tileMatrixNode, "ScaleDenominator"));
334                String[] topLeftCorner = getStringByXpath(tileMatrixNode, "TopLeftCorner").split(" ");
335
336                if (matrixProj.switchXY()) {
337                    tileMatrix.topLeftCorner = new EastNorth(Double.parseDouble(topLeftCorner[1]), Double.parseDouble(topLeftCorner[0]));
338                } else {
339                    tileMatrix.topLeftCorner = new EastNorth(Double.parseDouble(topLeftCorner[0]), Double.parseDouble(topLeftCorner[1]));
340                }
341                tileMatrix.tileHeight = Integer.parseInt(getStringByXpath(tileMatrixNode, "TileHeight"));
342                tileMatrix.tileWidth = Integer.parseInt(getStringByXpath(tileMatrixNode, "TileHeight"));
343                tileMatrix.matrixWidth = getOptionalIntegerByXpath(tileMatrixNode, "MatrixWidth");
344                tileMatrix.matrixHeight = getOptionalIntegerByXpath(tileMatrixNode, "MatrixHeight");
345                if (tileMatrix.tileHeight != tileMatrix.tileWidth) {
346                    throw new AssertionError(tr("Only square tiles are supported. {0}x{1} returned by server for TileMatrix identifier {2}",
347                            tileMatrix.tileHeight, tileMatrix.tileWidth, tileMatrix.identifier));
348                }
349
350                matrixSet.tileMatrix.add(tileMatrix);
351            }
352            ret.put(matrixSet.identifier, matrixSet);
353        }
354        return ret;
355    }
356
357    private static String crsToCode(String crsIdentifier) {
358        if (crsIdentifier.startsWith("urn:ogc:def:crs:")) {
359            return crsIdentifier.replaceFirst("urn:ogc:def:crs:([^:]*):.*:(.*)$", "$1:$2");
360        }
361        return crsIdentifier;
362    }
363
364    private static int getOptionalIntegerByXpath(Node document, String xpathQuery) throws XPathExpressionException {
365        String ret = getStringByXpath(document, xpathQuery);
366        if (ret == null || "".equals(ret)) {
367            return -1;
368        }
369        return Integer.parseInt(ret);
370    }
371
372    private static String getStringByXpath(Node document, String xpathQuery) throws XPathExpressionException {
373        return (String) getByXpath(document, xpathQuery, XPathConstants.STRING);
374    }
375
376    private static NodeList getByXpath(Node document, String xpathQuery) throws XPathExpressionException {
377        return (NodeList) getByXpath(document, xpathQuery, XPathConstants.NODESET);
378    }
379
380    private static Object getByXpath(Node document, String xpathQuery, QName returnType) throws XPathExpressionException {
381        XPath xpath = XPathFactory.newInstance().newXPath();
382        XPathExpression expr = xpath.compile(xpathQuery);
383        return expr.evaluate(document, returnType);
384    }
385
386    /**
387     * Initializes projection for this TileSource with projection
388     * @param proj projection to be used by this TileSource
389     */
390    public void initProjection(Projection proj) {
391        String layerName = null;
392        if (currentLayer != null) {
393            layerName = currentLayer.name;
394        }
395        Collection<Layer> candidates = getLayers(layerName, proj.toCode());
396        if (!candidates.isEmpty()) {
397            Layer newLayer = userSelectLayer(candidates);
398            if (newLayer != null) {
399                this.currentTileMatrixSet = newLayer.tileMatrixSet;
400                this.currentLayer = newLayer;
401            }
402        }
403
404        this.crsScale = getTileSize() * 0.28e-03 / proj.getMetersPerUnit();
405    }
406
407    private Collection<Layer> getLayers(String name, String projectionCode) {
408        Collection<Layer> ret = new ArrayList<>();
409        for (Layer layer: this.layers) {
410            if ((name == null || name.equals(layer.name)) && (projectionCode == null || projectionCode.equals(layer.tileMatrixSet.crs))) {
411                ret.add(layer);
412            }
413        }
414        return ret;
415    }
416
417    @Override
418    public int getDefaultTileSize() {
419        return getTileSize();
420    }
421
422    // FIXME: remove in September 2015, when ImageryPreferenceEntry.tileSize will be initialized to -1 instead to 256
423    // need to leave it as it is to keep compatiblity between tested and latest JOSM versions
424    @Override
425    public int getTileSize() {
426        TileMatrix matrix = getTileMatrix(1);
427        if (matrix == null) {
428            return 1;
429        }
430        return matrix.tileHeight;
431    }
432
433    @Override
434    public String getTileUrl(int zoom, int tilex, int tiley) {
435        String url;
436        if (currentLayer == null) {
437            return "";
438        }
439
440        switch (transferMode) {
441        case KVP:
442            url = baseUrl + URL_GET_ENCODING_PARAMS;
443            break;
444        case REST:
445            url = currentLayer.baseUrl;
446            break;
447        default:
448            url = "";
449            break;
450        }
451
452        TileMatrix tileMatrix = getTileMatrix(zoom);
453
454        if (tileMatrix == null) {
455            return ""; // no matrix, probably unsupported CRS selected.
456        }
457
458        return url.replaceAll("\\{layer\\}", this.currentLayer.name)
459                .replaceAll("\\{format\\}", this.currentLayer.format)
460                .replaceAll("\\{TileMatrixSet\\}", this.currentTileMatrixSet.identifier)
461                .replaceAll("\\{TileMatrix\\}", tileMatrix.identifier)
462                .replaceAll("\\{TileRow\\}", Integer.toString(tiley))
463                .replaceAll("\\{TileCol\\}", Integer.toString(tilex))
464                .replaceAll("\\{Style\\}", this.currentLayer.style);
465    }
466
467    /**
468     *
469     * @param zoom zoom level
470     * @return TileMatrix that's working on this zoom level
471     */
472    private TileMatrix getTileMatrix(int zoom) {
473        if (zoom > getMaxZoom()) {
474            return null;
475        }
476        if (zoom < 1) {
477            return null;
478        }
479        return this.currentTileMatrixSet.tileMatrix.toArray(new TileMatrix[]{})[zoom - 1];
480    }
481
482    @Override
483    public double getDistance(double lat1, double lon1, double lat2, double lon2) {
484        throw new UnsupportedOperationException("Not implemented");
485    }
486
487    @Override
488    public ICoordinate tileXYToLatLon(Tile tile) {
489        return tileXYToLatLon(tile.getXtile(), tile.getYtile(), tile.getZoom());
490    }
491
492    @Override
493    public ICoordinate tileXYToLatLon(TileXY xy, int zoom) {
494        return tileXYToLatLon(xy.getXIndex(), xy.getYIndex(), zoom);
495    }
496
497    @Override
498    public ICoordinate tileXYToLatLon(int x, int y, int zoom) {
499        TileMatrix matrix = getTileMatrix(zoom);
500        if (matrix == null) {
501            return Main.getProjection().getWorldBoundsLatLon().getCenter().toCoordinate();
502        }
503        double scale = matrix.scaleDenominator * this.crsScale;
504        EastNorth ret = new EastNorth(matrix.topLeftCorner.east() + x * scale, matrix.topLeftCorner.north() - y * scale);
505        return Main.getProjection().eastNorth2latlon(ret).toCoordinate();
506    }
507
508    @Override
509    public TileXY latLonToTileXY(double lat, double lon, int zoom) {
510        TileMatrix matrix = getTileMatrix(zoom);
511        if (matrix == null) {
512            return new TileXY(0, 0);
513        }
514
515        Projection proj = Main.getProjection();
516        EastNorth enPoint = proj.latlon2eastNorth(new LatLon(lat, lon));
517        double scale = matrix.scaleDenominator * this.crsScale;
518        return new TileXY(
519                (enPoint.east() - matrix.topLeftCorner.east()) / scale,
520                (matrix.topLeftCorner.north() - enPoint.north()) / scale
521                );
522    }
523
524    @Override
525    public TileXY latLonToTileXY(ICoordinate point, int zoom) {
526        return latLonToTileXY(point.getLat(),  point.getLon(), zoom);
527    }
528
529    @Override
530    public int getTileXMax(int zoom) {
531        return getTileXMax(zoom, Main.getProjection());
532    }
533
534    @Override
535    public int getTileXMin(int zoom) {
536        return 0;
537    }
538
539    @Override
540    public int getTileYMax(int zoom) {
541        return getTileYMax(zoom, Main.getProjection());
542    }
543
544    @Override
545    public int getTileYMin(int zoom) {
546        return 0;
547    }
548
549    @Override
550    public Point latLonToXY(double lat, double lon, int zoom) {
551        TileMatrix matrix = getTileMatrix(zoom);
552        if (matrix == null) {
553            return new Point(0, 0);
554        }
555        double scale = matrix.scaleDenominator * this.crsScale;
556        EastNorth point = Main.getProjection().latlon2eastNorth(new LatLon(lat, lon));
557        return new Point(
558                    (int) Math.round((point.east() - matrix.topLeftCorner.east())   / scale),
559                    (int) Math.round((matrix.topLeftCorner.north() - point.north()) / scale)
560                );
561    }
562
563    @Override
564    public Point latLonToXY(ICoordinate point, int zoom) {
565        return latLonToXY(point.getLat(), point.getLon(), zoom);
566    }
567
568    @Override
569    public Coordinate xyToLatLon(Point point, int zoom) {
570        return xyToLatLon(point.x, point.y, zoom);
571    }
572
573    @Override
574    public Coordinate xyToLatLon(int x, int y, int zoom) {
575        TileMatrix matrix = getTileMatrix(zoom);
576        if (matrix == null) {
577            return new Coordinate(0, 0);
578        }
579        double scale = matrix.scaleDenominator * this.crsScale;
580        Projection proj = Main.getProjection();
581        EastNorth ret = new EastNorth(
582                matrix.topLeftCorner.east() + x * scale,
583                matrix.topLeftCorner.north() - y * scale
584                );
585        LatLon ll = proj.eastNorth2latlon(ret);
586        return new Coordinate(ll.lat(), ll.lon());
587    }
588
589    @Override
590    public Map<String, String> getHeaders() {
591        return headers;
592    }
593
594    @Override
595    public int getMaxZoom() {
596        if (this.currentTileMatrixSet != null) {
597            return this.currentTileMatrixSet.tileMatrix.size();
598        }
599        return 0;
600    }
601
602    @Override
603    public String getTileId(int zoom, int tilex, int tiley) {
604        return getTileUrl(zoom, tilex, tiley);
605    }
606
607    /**
608     * Checks if url is acceptable by this Tile Source
609     * @param url URL to check
610     */
611    public static void checkUrl(String url) {
612        CheckParameterUtil.ensureParameterNotNull(url, "url");
613        Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);
614        while (m.find()) {
615            boolean isSupportedPattern = false;
616            for (String pattern : ALL_PATTERNS) {
617                if (m.group().matches(pattern)) {
618                    isSupportedPattern = true;
619                    break;
620                }
621            }
622            if (!isSupportedPattern) {
623                throw new IllegalArgumentException(
624                        tr("{0} is not a valid WMS argument. Please check this server URL:\n{1}", m.group(), url));
625            }
626        }
627    }
628
629    /**
630     * @return set of projection codes that this TileSource supports
631     */
632    public Set<String> getSupportedProjections() {
633        Set<String> ret = new HashSet<>();
634        if (currentLayer == null) {
635            for (Layer layer: this.layers) {
636                ret.add(layer.tileMatrixSet.crs);
637            }
638        } else {
639            for (Layer layer: this.layers) {
640                if (currentLayer.name.equals(layer.name)) {
641                    ret.add(layer.tileMatrixSet.crs);
642                }
643            }
644        }
645        return ret;
646    }
647
648    private int getTileYMax(int zoom, Projection proj) {
649        TileMatrix matrix = getTileMatrix(zoom);
650        if (matrix == null) {
651            return 0;
652        }
653
654        if (matrix.matrixHeight != -1) {
655            return matrix.matrixHeight;
656        }
657
658        double scale = matrix.scaleDenominator * this.crsScale;
659        EastNorth min = matrix.topLeftCorner;
660        EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax());
661        return (int) Math.ceil(Math.abs(max.north() - min.north()) / scale);
662    }
663
664    private int getTileXMax(int zoom, Projection proj) {
665        TileMatrix matrix = getTileMatrix(zoom);
666        if (matrix == null) {
667            return 0;
668        }
669        if (matrix.matrixWidth != -1) {
670            return matrix.matrixWidth;
671        }
672
673        double scale = matrix.scaleDenominator * this.crsScale;
674        EastNorth min = matrix.topLeftCorner;
675        EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax());
676        return (int) Math.ceil(Math.abs(max.east() - min.east()) / scale);
677    }
678}