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

Last change on this file since 19050 was 19050, checked in by taylor.smock, 4 weeks ago

Revert most var changes from r19048, fix most new compile warnings and checkstyle issues

Also, document why various ErrorProne checks were originally disabled and fix
generic SonarLint issues.

  • Property svn:eol-style set to native
File size: 49.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.imagery;
3
4import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
5import static javax.xml.stream.XMLStreamConstants.START_ELEMENT;
6import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_DCP;
7import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_GET;
8import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_HTTP;
9import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER;
10import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_LOWER_CORNER;
11import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_OPERATION;
12import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_OPERATIONS_METADATA;
13import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_SUPPORTED_CRS;
14import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_TITLE;
15import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_UPPER_CORNER;
16import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_WGS84_BOUNDING_BOX;
17import static org.openstreetmap.josm.tools.I18n.tr;
18
19import java.awt.Point;
20import java.io.ByteArrayInputStream;
21import java.io.IOException;
22import java.io.InputStream;
23import java.nio.charset.StandardCharsets;
24import java.nio.file.InvalidPathException;
25import java.util.ArrayList;
26import java.util.Arrays;
27import java.util.Collection;
28import java.util.Collections;
29import java.util.Deque;
30import java.util.HashMap;
31import java.util.LinkedHashSet;
32import java.util.LinkedList;
33import java.util.List;
34import java.util.Map;
35import java.util.Map.Entry;
36import java.util.Objects;
37import java.util.Optional;
38import java.util.SortedSet;
39import java.util.TreeSet;
40import java.util.concurrent.ConcurrentHashMap;
41import java.util.function.BiFunction;
42import java.util.stream.Collectors;
43
44import javax.imageio.ImageIO;
45import javax.swing.ListSelectionModel;
46import javax.xml.namespace.QName;
47import javax.xml.stream.XMLStreamException;
48import javax.xml.stream.XMLStreamReader;
49
50import org.openstreetmap.gui.jmapviewer.Coordinate;
51import org.openstreetmap.gui.jmapviewer.Projected;
52import org.openstreetmap.gui.jmapviewer.Tile;
53import org.openstreetmap.gui.jmapviewer.TileRange;
54import org.openstreetmap.gui.jmapviewer.TileXY;
55import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
56import org.openstreetmap.gui.jmapviewer.interfaces.IProjected;
57import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
58import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
59import org.openstreetmap.josm.data.ProjectionBounds;
60import org.openstreetmap.josm.data.coor.EastNorth;
61import org.openstreetmap.josm.data.coor.LatLon;
62import org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.TransferMode;
63import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
64import org.openstreetmap.josm.data.osm.BBox;
65import org.openstreetmap.josm.data.projection.Projection;
66import org.openstreetmap.josm.data.projection.ProjectionRegistry;
67import org.openstreetmap.josm.data.projection.Projections;
68import org.openstreetmap.josm.gui.ExtendedDialog;
69import org.openstreetmap.josm.gui.MainApplication;
70import org.openstreetmap.josm.gui.layer.NativeScaleLayer.ScaleList;
71import org.openstreetmap.josm.gui.layer.imagery.WMTSLayerSelection;
72import org.openstreetmap.josm.io.CachedFile;
73import org.openstreetmap.josm.spi.preferences.Config;
74import org.openstreetmap.josm.tools.CheckParameterUtil;
75import org.openstreetmap.josm.tools.Logging;
76
77/**
78 * Tile Source handling WMTS providers
79 *
80 * @author Wiktor Niesiobędzki
81 * @since 8526
82 */
83public class WMTSTileSource extends AbstractTMSTileSource implements TemplatedTileSource {
84 /**
85 * WMTS namespace address
86 */
87 public static final String WMTS_NS_URL = "http://www.opengis.net/wmts/1.0";
88
89 // CHECKSTYLE.OFF: SingleSpaceSeparator
90 private static final QName QN_CONTENTS = new QName(WMTS_NS_URL, "Contents");
91 private static final QName QN_DEFAULT = new QName(WMTS_NS_URL, "Default");
92 private static final QName QN_DIMENSION = new QName(WMTS_NS_URL, "Dimension");
93 private static final QName QN_FORMAT = new QName(WMTS_NS_URL, "Format");
94 private static final QName QN_LAYER = new QName(WMTS_NS_URL, "Layer");
95 private static final QName QN_MATRIX_WIDTH = new QName(WMTS_NS_URL, "MatrixWidth");
96 private static final QName QN_MATRIX_HEIGHT = new QName(WMTS_NS_URL, "MatrixHeight");
97 private static final QName QN_RESOURCE_URL = new QName(WMTS_NS_URL, "ResourceURL");
98 private static final QName QN_SCALE_DENOMINATOR = new QName(WMTS_NS_URL, "ScaleDenominator");
99 private static final QName QN_STYLE = new QName(WMTS_NS_URL, "Style");
100 private static final QName QN_TILEMATRIX = new QName(WMTS_NS_URL, "TileMatrix");
101 private static final QName QN_TILEMATRIXSET = new QName(WMTS_NS_URL, "TileMatrixSet");
102 private static final QName QN_TILEMATRIX_SET_LINK = new QName(WMTS_NS_URL, "TileMatrixSetLink");
103 private static final QName QN_TILE_WIDTH = new QName(WMTS_NS_URL, "TileWidth");
104 private static final QName QN_TILE_HEIGHT = new QName(WMTS_NS_URL, "TileHeight");
105 private static final QName QN_TOPLEFT_CORNER = new QName(WMTS_NS_URL, "TopLeftCorner");
106 private static final QName QN_VALUE = new QName(WMTS_NS_URL, "Value");
107 // CHECKSTYLE.ON: SingleSpaceSeparator
108
109 private static final String URL_GET_ENCODING_PARAMS = "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER={layer}&STYLE={style}&"
110 + "FORMAT={format}&tileMatrixSet={TileMatrixSet}&tileMatrix={TileMatrix}&tileRow={TileRow}&tileCol={TileCol}";
111
112 private int cachedTileSize = -1;
113
114 private static final class TileMatrix {
115 private String identifier;
116 private double scaleDenominator;
117 private EastNorth topLeftCorner;
118 private int tileWidth;
119 private int tileHeight;
120 private int matrixWidth = -1;
121 private int matrixHeight = -1;
122 }
123
124 private static final class TileMatrixSetBuilder {
125 // sorted by zoom level
126 SortedSet<TileMatrix> tileMatrix = new TreeSet<>((o1, o2) -> -1 * Double.compare(o1.scaleDenominator, o2.scaleDenominator));
127 private String crs;
128 private String identifier;
129
130 TileMatrixSet build() {
131 return new TileMatrixSet(this);
132 }
133 }
134
135 /**
136 * class representing WMTS TileMatrixSet
137 * This connects projection and TileMatrix (how the map is divided in tiles)
138 * @since 13733
139 */
140 public static class TileMatrixSet {
141
142 private final List<TileMatrix> tileMatrix;
143 private final String crs;
144 private final String identifier;
145
146 TileMatrixSet(TileMatrixSet tileMatrixSet) {
147 if (tileMatrixSet != null) {
148 tileMatrix = new ArrayList<>(tileMatrixSet.tileMatrix);
149 crs = tileMatrixSet.crs;
150 identifier = tileMatrixSet.identifier;
151 } else {
152 tileMatrix = Collections.emptyList();
153 crs = null;
154 identifier = null;
155 }
156 }
157
158 TileMatrixSet(TileMatrixSetBuilder builder) {
159 tileMatrix = new ArrayList<>(builder.tileMatrix);
160 crs = builder.crs;
161 identifier = builder.identifier;
162 }
163
164 @Override
165 public String toString() {
166 return "TileMatrixSet [crs=" + crs + ", identifier=" + identifier + ']';
167 }
168
169 /**
170 * Returns identifier of this TileMatrixSet.
171 * @return identifier of this TileMatrixSet
172 */
173 public String getIdentifier() {
174 return identifier;
175 }
176
177 /**
178 * Returns projection of this tileMatrix.
179 * @return projection of this tileMatrix
180 */
181 public String getCrs() {
182 return crs;
183 }
184
185 /**
186 * Returns tile matrix max zoom. Assumes first zoom starts at 0, with continuous zoom levels.
187 * @return tile matrix max zoom
188 * @since 15409
189 */
190 public int getMaxZoom() {
191 return tileMatrix.size() - 1;
192 }
193 }
194
195 private static final class Dimension {
196 private String identifier;
197 private String defaultValue;
198 private final List<String> values = new ArrayList<>();
199 }
200
201 /**
202 * Class representing WMTS Layer information
203 * @since 13733
204 */
205 public static class Layer {
206 private String format;
207 private String identifier;
208 private String title;
209 private TileMatrixSet tileMatrixSet;
210 private String baseUrl;
211 private String style;
212 private BBox bbox;
213 private final Collection<String> tileMatrixSetLinks = new ArrayList<>();
214 private final Collection<Dimension> dimensions = new ArrayList<>();
215
216 Layer(Layer l) {
217 Objects.requireNonNull(l);
218 format = l.format;
219 identifier = l.identifier;
220 title = l.title;
221 baseUrl = l.baseUrl;
222 style = l.style;
223 bbox = l.bbox;
224 tileMatrixSet = new TileMatrixSet(l.tileMatrixSet);
225 dimensions.addAll(l.dimensions);
226 }
227
228 Layer() {
229 }
230
231 /**
232 * Get title of the layer for user display.
233 * <p>
234 * This is either the content of the Title element (if available) or
235 * the layer identifier (as fallback)
236 * @return title of the layer for user display
237 */
238 public String getUserTitle() {
239 return title != null ? title : identifier;
240 }
241
242 @Override
243 public String toString() {
244 return "Layer [identifier=" + identifier + ", title=" + title + ", tileMatrixSet="
245 + tileMatrixSet + ", baseUrl=" + baseUrl + ", style=" + style + ']';
246 }
247
248 /**
249 * Returns identifier of this layer.
250 * @return identifier of this layer
251 */
252 public String getIdentifier() {
253 return identifier;
254 }
255
256 /**
257 * Returns style of this layer.
258 * @return style of this layer
259 */
260 public String getStyle() {
261 return style;
262 }
263
264 /**
265 * Returns tileMatrixSet of this layer.
266 * @return tileMatrixSet of this layer
267 */
268 public TileMatrixSet getTileMatrixSet() {
269 return tileMatrixSet;
270 }
271
272 /**
273 * Returns layer max zoom.
274 * @return layer max zoom
275 * @since 15409
276 */
277 public int getMaxZoom() {
278 return tileMatrixSet != null ? tileMatrixSet.getMaxZoom() : 0;
279 }
280
281 /**
282 * Returns the WGS84 bounding box.
283 * @return WGS84 bounding box
284 * @since 15410
285 */
286 public BBox getBbox() {
287 return bbox;
288 }
289 }
290
291 /**
292 * Exception thrown when parser doesn't find expected information in GetCapabilities document
293 * @since 13733
294 */
295 public static class WMTSGetCapabilitiesException extends Exception {
296
297 /**
298 * Create WMTS exception
299 * @param cause description of cause
300 */
301 public WMTSGetCapabilitiesException(String cause) {
302 super(cause);
303 }
304
305 /**
306 * Create WMTS exception
307 * @param cause description of cause
308 * @param t nested exception
309 */
310 public WMTSGetCapabilitiesException(String cause, Throwable t) {
311 super(cause, t);
312 }
313 }
314
315 private static final class SelectLayerDialog extends ExtendedDialog {
316 private final WMTSLayerSelection list;
317
318 SelectLayerDialog(Collection<Layer> layers) {
319 super(MainApplication.getMainFrame(), tr("Select WMTS layer"), tr("Add layers"), tr("Cancel"));
320 this.list = new WMTSLayerSelection(groupLayersByNameAndTileMatrixSet(layers));
321 setContent(list);
322 }
323
324 @Override
325 public void setupDialog() {
326 super.setupDialog();
327 buttons.get(0).setEnabled(false);
328 ListSelectionModel selectionModel = list.getTable().getSelectionModel();
329 selectionModel.addListSelectionListener(e -> buttons.get(0).setEnabled(!selectionModel.isSelectionEmpty()));
330 }
331
332 public DefaultLayer getSelectedLayer() {
333 Layer selectedLayer = list.getSelectedLayer();
334 return selectedLayer == null ? null :
335 new DefaultLayer(ImageryType.WMTS, selectedLayer.identifier, selectedLayer.style, selectedLayer.tileMatrixSet.identifier);
336 }
337 }
338
339 private final Map<String, String> headers = new ConcurrentHashMap<>();
340 private final Collection<Layer> layers;
341 private Layer currentLayer;
342 private TileMatrixSet currentTileMatrixSet;
343 private double crsScale;
344 private final TransferMode transferMode;
345
346 private ScaleList nativeScaleList;
347
348 private final DefaultLayer defaultLayer;
349
350 private Projection tileProjection;
351
352 /**
353 * Creates a tile source based on imagery info
354 * @param info imagery info
355 * @throws IOException if any I/O error occurs
356 * @throws WMTSGetCapabilitiesException when document didn't contain any layers
357 * @throws IllegalArgumentException if any other error happens for the given imagery info
358 */
359 public WMTSTileSource(ImageryInfo info) throws IOException, WMTSGetCapabilitiesException {
360 super(info);
361 CheckParameterUtil.ensureThat(info.getDefaultLayers().size() < 2, "At most 1 default layer for WMTS is supported");
362 this.headers.putAll(info.getCustomHttpHeaders());
363 this.baseUrl = GetCapabilitiesParseHelper.normalizeCapabilitiesUrl(
364 ImageryPatterns.handleApiKeyTemplate(info.getId(), ImageryPatterns.handleHeaderTemplate(info.getUrl(), headers)));
365 WMTSCapabilities capabilities = getCapabilities(baseUrl, headers);
366 this.layers = capabilities.getLayers();
367 this.baseUrl = capabilities.getBaseUrl();
368 this.transferMode = capabilities.getTransferMode();
369 if (info.getDefaultLayers().isEmpty()) {
370 Logging.warn(tr("No default layer selected, choosing first layer."));
371 if (!layers.isEmpty()) {
372 Layer first = layers.iterator().next();
373 // If max zoom lower than expected, try to find a better layer
374 final int maxZoom = info.getMaxZoom();
375 if (first.getMaxZoom() < maxZoom) {
376 first = layers.stream().filter(l -> l.getMaxZoom() >= maxZoom).findFirst().orElse(first);
377 }
378 // If center of josm bbox not in layer bbox, try to find a better layer
379 if (info.getBounds() != null && first.getBbox() != null) {
380 LatLon center = info.getBounds().getCenter();
381 if (!first.getBbox().bounds(center)) {
382 final Layer ffirst = first;
383 first = layers.stream()
384 .filter(l -> l.getMaxZoom() >= maxZoom && l.getBbox() != null && l.getBbox().bounds(center)).findFirst()
385 .orElseGet(() -> layers.stream().filter(l -> l.getBbox() != null && l.getBbox().bounds(center)).findFirst()
386 .orElse(ffirst));
387 }
388 }
389 this.defaultLayer = new DefaultLayer(info.getImageryType(), first.identifier, first.style, first.tileMatrixSet.identifier);
390 } else {
391 this.defaultLayer = null;
392 }
393 } else {
394 this.defaultLayer = info.getDefaultLayers().get(0);
395 }
396 if (this.layers.isEmpty())
397 throw new IllegalArgumentException(tr("No layers defined by getCapabilities document: {0}", info.getUrl()));
398 }
399
400 /**
401 * Creates a tile source based on imagery info and initializes it with given projection.
402 * @param info imagery info
403 * @param projection projection to be used by this TileSource
404 * @throws IOException if any I/O error occurs
405 * @throws WMTSGetCapabilitiesException when document didn't contain any layers
406 * @throws IllegalArgumentException if any other error happens for the given imagery info
407 * @since 14507
408 */
409 public WMTSTileSource(ImageryInfo info, Projection projection) throws IOException, WMTSGetCapabilitiesException {
410 this(info);
411 initProjection(projection);
412 }
413
414 /**
415 * Creates a dialog based on this tile source with all available layers and returns the name of selected layer
416 * @return Name of selected layer
417 */
418 public DefaultLayer userSelectLayer() {
419 Map<String, List<Layer>> layerById = layers.stream().collect(
420 Collectors.groupingBy(x -> x.identifier));
421 if (layerById.size() == 1) { // only one layer
422 List<Layer> ls = layerById.entrySet().iterator().next().getValue()
423 .stream().filter(
424 u -> u.tileMatrixSet.crs.equals(ProjectionRegistry.getProjection().toCode()))
425 .collect(Collectors.toList());
426 if (ls.size() == 1) {
427 // only one tile matrix set with matching projection - no point in asking
428 Layer selectedLayer = ls.get(0);
429 return new DefaultLayer(ImageryType.WMTS, selectedLayer.identifier, selectedLayer.style, selectedLayer.tileMatrixSet.identifier);
430 }
431 }
432
433 final SelectLayerDialog layerSelection = new SelectLayerDialog(layers);
434 if (layerSelection.showDialog().getValue() == 1) {
435 return layerSelection.getSelectedLayer();
436 }
437 return null;
438 }
439
440 /**
441 * Call remote server and parse response to WMTSCapabilities object
442 *
443 * @param url of the getCapabilities document
444 * @param headers HTTP headers to set when calling getCapabilities url
445 * @return capabilities
446 * @throws IOException in case of any I/O error
447 * @throws WMTSGetCapabilitiesException when document didn't contain any layers
448 * @throws IllegalArgumentException in case of any other error
449 */
450 public static WMTSCapabilities getCapabilities(String url, Map<String, String> headers) throws IOException, WMTSGetCapabilitiesException {
451 try (CachedFile cf = new CachedFile(url); InputStream in = cf.setHttpHeaders(headers).
452 setMaxAge(Config.getPref().getLong("wmts.capabilities.cache.max_age", 7 * CachedFile.DAYS)).
453 setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince).
454 getInputStream()) {
455 byte[] data = in.readAllBytes();
456 if (data.length == 0) {
457 cf.clear();
458 throw new IllegalArgumentException("Could not read data from: " + url);
459 }
460
461 try {
462 XMLStreamReader reader = GetCapabilitiesParseHelper.getReader(new ByteArrayInputStream(data));
463 WMTSCapabilities ret = null;
464 Collection<Layer> layers = null;
465 for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) {
466 if (event == START_ELEMENT) {
467 QName qName = reader.getName();
468 if (QN_OWS_OPERATIONS_METADATA.equals(qName)) {
469 ret = parseOperationMetadata(reader);
470 } else if (QN_CONTENTS.equals(qName)) {
471 layers = parseContents(reader);
472 }
473 }
474 }
475 if (ret == null) {
476 /*
477 * see #12168 - create dummy operation metadata - not all WMTS services provide this information
478 *
479 * WMTS Standard:
480 * > Resource oriented architecture style HTTP encodings SHALL not be described in the OperationsMetadata section.
481 *
482 * And OperationMetada is not mandatory element. So REST mode is justifiable
483 */
484 ret = new WMTSCapabilities(url, TransferMode.REST);
485 }
486 if (layers == null) {
487 throw new WMTSGetCapabilitiesException(tr("WMTS Capabilities document did not contain layers in url: {0}", url));
488 }
489 ret.addLayers(layers);
490 return ret;
491 } catch (XMLStreamException e) {
492 cf.clear();
493 Logging.warn(new String(data, StandardCharsets.UTF_8));
494 throw new WMTSGetCapabilitiesException(tr("Error during parsing of WMTS Capabilities document: {0}", e.getMessage()), e);
495 }
496 } catch (InvalidPathException e) {
497 throw new WMTSGetCapabilitiesException(tr("Invalid path for GetCapabilities document: {0}", e.getMessage()), e);
498 }
499 }
500
501 /**
502 * Parse Contents tag. Returns when reader reaches Contents closing tag
503 *
504 * @param reader StAX reader instance
505 * @return collection of layers within contents with properly linked TileMatrixSets
506 * @throws XMLStreamException See {@link XMLStreamReader}
507 */
508 private static Collection<Layer> parseContents(XMLStreamReader reader) throws XMLStreamException {
509 Map<String, TileMatrixSet> matrixSetById = new HashMap<>();
510 Collection<Layer> layers = new ArrayList<>();
511 for (int event = reader.getEventType();
512 reader.hasNext() && !(event == END_ELEMENT && QN_CONTENTS.equals(reader.getName()));
513 event = reader.next()) {
514 if (event == START_ELEMENT) {
515 QName qName = reader.getName();
516 if (QN_LAYER.equals(qName)) {
517 Layer l = parseLayer(reader);
518 if (l != null) {
519 layers.add(l);
520 }
521 } else if (QN_TILEMATRIXSET.equals(qName)) {
522 TileMatrixSet entry = parseTileMatrixSet(reader);
523 matrixSetById.put(entry.identifier, entry);
524 }
525 }
526 }
527 Collection<Layer> ret = new ArrayList<>();
528 // link layers to matrix sets
529 for (Layer l: layers) {
530 for (String tileMatrixId: l.tileMatrixSetLinks) {
531 Layer newLayer = new Layer(l); // create a new layer object for each tile matrix set supported
532 newLayer.tileMatrixSet = matrixSetById.get(tileMatrixId);
533 ret.add(newLayer);
534 }
535 }
536 return ret;
537 }
538
539 /**
540 * Parse Layer tag. Returns when reader will reach Layer closing tag
541 *
542 * @param reader StAX reader instance
543 * @return Layer object, with tileMatrixSetLinks and no tileMatrixSet attribute set.
544 * @throws XMLStreamException See {@link XMLStreamReader}
545 */
546 private static Layer parseLayer(XMLStreamReader reader) throws XMLStreamException {
547 Layer layer = new Layer();
548 Deque<QName> tagStack = new LinkedList<>();
549 List<String> supportedMimeTypes = new ArrayList<>(Arrays.asList(ImageIO.getReaderMIMETypes()));
550 supportedMimeTypes.add("image/jpgpng"); // used by ESRI
551 supportedMimeTypes.add("image/png8"); // used by geoserver
552 supportedMimeTypes.add("image/vnd.jpeg-png"); // used by geoserver
553 supportedMimeTypes.add("image/vnd.jpeg-png8"); // used by geoserver
554 supportedMimeTypes.add("image/png; mode=8bit"); // used by MapServer
555 if (supportedMimeTypes.contains("image/jpeg")) {
556 supportedMimeTypes.add("image/jpg"); // sometimes misspelled by Arcgis
557 }
558 Collection<String> unsupportedFormats = new ArrayList<>();
559
560 for (int event = reader.getEventType();
561 reader.hasNext() && !(event == END_ELEMENT && QN_LAYER.equals(reader.getName()));
562 event = reader.next()) {
563 if (event == START_ELEMENT) {
564 QName qName = reader.getName();
565 tagStack.push(qName);
566 if (tagStack.size() == 2) {
567 if (QN_FORMAT.equals(qName)) {
568 String format = reader.getElementText();
569 if (supportedMimeTypes.contains(format)) {
570 layer.format = format;
571 } else {
572 unsupportedFormats.add(format);
573 }
574 } else if (QN_OWS_IDENTIFIER.equals(qName)) {
575 layer.identifier = reader.getElementText();
576 } else if (QN_OWS_TITLE.equals(qName)) {
577 layer.title = reader.getElementText();
578 } else if (QN_RESOURCE_URL.equals(qName) &&
579 "tile".equals(reader.getAttributeValue("", "resourceType"))) {
580 layer.baseUrl = reader.getAttributeValue("", "template");
581 } else if (QN_STYLE.equals(qName) &&
582 "true".equals(reader.getAttributeValue("", "isDefault"))) {
583 if (GetCapabilitiesParseHelper.moveReaderToTag(reader, QN_OWS_IDENTIFIER)) {
584 layer.style = reader.getElementText();
585 tagStack.push(reader.getName()); // keep tagStack in sync
586 }
587 } else if (QN_DIMENSION.equals(qName)) {
588 layer.dimensions.add(parseDimension(reader));
589 } else if (QN_TILEMATRIX_SET_LINK.equals(qName)) {
590 layer.tileMatrixSetLinks.add(parseTileMatrixSetLink(reader));
591 } else if (QN_OWS_WGS84_BOUNDING_BOX.equals(qName)) {
592 layer.bbox = parseBoundingBox(reader);
593 } else {
594 GetCapabilitiesParseHelper.moveReaderToEndCurrentTag(reader);
595 }
596 }
597 }
598 // need to get event type from reader, as parsing might have change position of reader
599 if (reader.getEventType() == END_ELEMENT) {
600 QName start = tagStack.pop();
601 if (!start.equals(reader.getName())) {
602 throw new IllegalStateException(tr("WMTS Parser error - start element {0} has different name than end element {2}",
603 start, reader.getName()));
604 }
605 }
606 }
607 if (layer.style == null) {
608 layer.style = "";
609 }
610 if (layer.format == null) {
611 // no format found - it's mandatory parameter - can't use this layer
612 Logging.warn(tr("Can''t use layer {0} because no supported formats were found. Layer is available in formats: {1}",
613 layer.getUserTitle(),
614 String.join(", ", unsupportedFormats)));
615 return null;
616 }
617 // Java has issues if spaces are not URL encoded. Ensure that we URL encode the spaces.
618 if (layer.format.contains(" ")) {
619 layer.format = layer.format.replace(" ", "%20");
620 }
621 return layer;
622 }
623
624 /**
625 * Gets Dimension value. Returns when reader is on Dimension closing tag
626 *
627 * @param reader StAX reader instance
628 * @return dimension
629 * @throws XMLStreamException See {@link XMLStreamReader}
630 */
631 private static Dimension parseDimension(XMLStreamReader reader) throws XMLStreamException {
632 Dimension ret = new Dimension();
633 for (int event = reader.getEventType();
634 reader.hasNext() && !(event == END_ELEMENT && QN_DIMENSION.equals(reader.getName()));
635 event = reader.next()) {
636 if (event == START_ELEMENT) {
637 QName qName = reader.getName();
638 if (QN_OWS_IDENTIFIER.equals(qName)) {
639 ret.identifier = reader.getElementText();
640 } else if (QN_DEFAULT.equals(qName)) {
641 ret.defaultValue = reader.getElementText();
642 } else if (QN_VALUE.equals(qName)) {
643 ret.values.add(reader.getElementText());
644 }
645 }
646 }
647 return ret;
648 }
649
650 /**
651 * Gets TileMatrixSetLink value. Returns when reader is on TileMatrixSetLink closing tag
652 *
653 * @param reader StAX reader instance
654 * @return TileMatrixSetLink identifier
655 * @throws XMLStreamException See {@link XMLStreamReader}
656 */
657 private static String parseTileMatrixSetLink(XMLStreamReader reader) throws XMLStreamException {
658 String ret = null;
659 for (int event = reader.getEventType();
660 reader.hasNext() && !(event == END_ELEMENT && QN_TILEMATRIX_SET_LINK.equals(reader.getName()));
661 event = reader.next()) {
662 if (event == START_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName())) {
663 ret = reader.getElementText();
664 }
665 }
666 return ret;
667 }
668
669 /**
670 * Parses TileMatrixSet section. Returns when reader is on TileMatrixSet closing tag
671 * @param reader StAX reader instance
672 * @return TileMatrixSet object
673 * @throws XMLStreamException See {@link XMLStreamReader}
674 */
675 private static TileMatrixSet parseTileMatrixSet(XMLStreamReader reader) throws XMLStreamException {
676 TileMatrixSetBuilder matrixSet = new TileMatrixSetBuilder();
677 for (int event = reader.getEventType();
678 reader.hasNext() && !(event == END_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName()));
679 event = reader.next()) {
680 if (event == START_ELEMENT) {
681 QName qName = reader.getName();
682 if (QN_OWS_IDENTIFIER.equals(qName)) {
683 matrixSet.identifier = reader.getElementText();
684 } else if (QN_OWS_SUPPORTED_CRS.equals(qName)) {
685 matrixSet.crs = GetCapabilitiesParseHelper.crsToCode(reader.getElementText());
686 } else if (QN_TILEMATRIX.equals(qName)) {
687 matrixSet.tileMatrix.add(parseTileMatrix(reader, matrixSet.crs));
688 }
689 }
690 }
691 return matrixSet.build();
692 }
693
694 /**
695 * Parses TileMatrix section. Returns when reader is on TileMatrix closing tag.
696 * @param reader StAX reader instance
697 * @param matrixCrs projection used by this matrix
698 * @return TileMatrix object
699 * @throws XMLStreamException See {@link XMLStreamReader}
700 */
701 private static TileMatrix parseTileMatrix(XMLStreamReader reader, String matrixCrs) throws XMLStreamException {
702 Projection matrixProj = Optional.ofNullable(Projections.getProjectionByCode(matrixCrs))
703 .orElseGet(ProjectionRegistry::getProjection); // use current projection if none found. Maybe user is using custom string
704 TileMatrix ret = new TileMatrix();
705 for (int event = reader.getEventType();
706 reader.hasNext() && !(event == END_ELEMENT && QN_TILEMATRIX.equals(reader.getName()));
707 event = reader.next()) {
708 if (event == START_ELEMENT) {
709 QName qName = reader.getName();
710 if (QN_OWS_IDENTIFIER.equals(qName)) {
711 ret.identifier = reader.getElementText();
712 } else if (QN_SCALE_DENOMINATOR.equals(qName)) {
713 ret.scaleDenominator = Double.parseDouble(reader.getElementText());
714 } else if (QN_TOPLEFT_CORNER.equals(qName)) {
715 ret.topLeftCorner = parseEastNorth(reader.getElementText(), matrixProj.switchXY());
716 } else if (QN_TILE_HEIGHT.equals(qName)) {
717 ret.tileHeight = Integer.parseInt(reader.getElementText());
718 } else if (QN_TILE_WIDTH.equals(qName)) {
719 ret.tileWidth = Integer.parseInt(reader.getElementText());
720 } else if (QN_MATRIX_HEIGHT.equals(qName)) {
721 ret.matrixHeight = Integer.parseInt(reader.getElementText());
722 } else if (QN_MATRIX_WIDTH.equals(qName)) {
723 ret.matrixWidth = Integer.parseInt(reader.getElementText());
724 }
725 }
726 }
727 if (ret.tileHeight != ret.tileWidth) {
728 throw new AssertionError(tr("Only square tiles are supported. {0}x{1} returned by server for TileMatrix identifier {2}",
729 ret.tileHeight, ret.tileWidth, ret.identifier));
730 }
731 return ret;
732 }
733
734 private static <T> T parseCoor(String coor, boolean switchXY, BiFunction<String, String, T> function) {
735 String[] parts = coor.split(" ", -1);
736 if (switchXY) {
737 return function.apply(parts[1], parts[0]);
738 } else {
739 return function.apply(parts[0], parts[1]);
740 }
741 }
742
743 private static EastNorth parseEastNorth(String coor, boolean switchXY) {
744 return parseCoor(coor, switchXY, (e, n) -> new EastNorth(Double.parseDouble(e), Double.parseDouble(n)));
745 }
746
747 private static LatLon parseLatLon(String coor, boolean switchXY) {
748 return parseCoor(coor, switchXY, (lon, lat) -> new LatLon(Double.parseDouble(lat), Double.parseDouble(lon)));
749 }
750
751 /**
752 * Parses WGS84BoundingBox section. Returns when reader is on WGS84BoundingBox closing tag.
753 * @param reader StAX reader instance
754 * @return WGS84 bounding box
755 * @throws XMLStreamException See {@link XMLStreamReader}
756 */
757 private static BBox parseBoundingBox(XMLStreamReader reader) throws XMLStreamException {
758 LatLon lowerCorner = null;
759 LatLon upperCorner = null;
760 for (int event = reader.getEventType();
761 reader.hasNext() && !(event == END_ELEMENT && QN_OWS_WGS84_BOUNDING_BOX.equals(reader.getName()));
762 event = reader.next()) {
763 if (event == START_ELEMENT) {
764 QName qName = reader.getName();
765 if (QN_OWS_LOWER_CORNER.equals(qName)) {
766 lowerCorner = parseLatLon(reader.getElementText(), false);
767 } else if (QN_OWS_UPPER_CORNER.equals(qName)) {
768 upperCorner = parseLatLon(reader.getElementText(), false);
769 }
770 }
771 }
772 if (lowerCorner != null && upperCorner != null) {
773 return new BBox(lowerCorner, upperCorner);
774 }
775 return null;
776 }
777
778 /**
779 * Parses OperationMetadata section. Returns when reader is on OperationsMetadata closing tag.
780 * return WMTSCapabilities with baseUrl and transferMode
781 *
782 * @param reader StAX reader instance
783 * @return WMTSCapabilities with baseUrl and transferMode set
784 * @throws XMLStreamException See {@link XMLStreamReader}
785 */
786 private static WMTSCapabilities parseOperationMetadata(XMLStreamReader reader) throws XMLStreamException {
787 for (int event = reader.getEventType();
788 reader.hasNext() && !(event == END_ELEMENT && QN_OWS_OPERATIONS_METADATA.equals(reader.getName()));
789 event = reader.next()) {
790 if (event == START_ELEMENT &&
791 QN_OWS_OPERATION.equals(reader.getName()) &&
792 "GetTile".equals(reader.getAttributeValue("", "name")) &&
793 GetCapabilitiesParseHelper.moveReaderToTag(reader, QN_OWS_DCP, QN_OWS_HTTP, QN_OWS_GET)) {
794 return new WMTSCapabilities(
795 reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href"),
796 GetCapabilitiesParseHelper.getTransferMode(reader)
797 );
798 }
799 }
800 return null;
801 }
802
803 /**
804 * Initializes projection for this TileSource with projection
805 * @param proj projection to be used by this TileSource
806 */
807 public void initProjection(Projection proj) {
808 if (proj.equals(tileProjection))
809 return;
810 List<Layer> matchingLayers = layers.stream().filter(
811 l -> l.identifier.equals(defaultLayer.getLayerName()) && l.tileMatrixSet.crs.equals(proj.toCode()))
812 .collect(Collectors.toList());
813 if (matchingLayers.size() > 1) {
814 this.currentLayer = matchingLayers.stream().filter(
815 l -> l.tileMatrixSet.identifier.equals(defaultLayer.getTileMatrixSet()))
816 .findFirst().orElse(matchingLayers.get(0));
817 this.tileProjection = proj;
818 } else if (matchingLayers.size() == 1) {
819 this.currentLayer = matchingLayers.get(0);
820 this.tileProjection = proj;
821 } else {
822 // no tile matrix sets with current projection
823 if (this.currentLayer == null) {
824 this.tileProjection = null;
825 for (Layer layer : layers) {
826 if (!layer.identifier.equals(defaultLayer.getLayerName())) {
827 continue;
828 }
829 Projection pr = Projections.getProjectionByCode(layer.tileMatrixSet.crs);
830 if (pr != null) {
831 this.currentLayer = layer;
832 this.tileProjection = pr;
833 break;
834 }
835 }
836 if (this.currentLayer == null)
837 throw new IllegalArgumentException(
838 layers.stream().map(l -> l.tileMatrixSet).collect(Collectors.toList()).toString());
839 } // else: keep currentLayer and tileProjection as is
840 }
841 if (this.currentLayer != null) {
842 this.currentTileMatrixSet = this.currentLayer.tileMatrixSet;
843 Collection<Double> scales = currentTileMatrixSet.tileMatrix.stream()
844 .map(tileMatrix -> tileMatrix.scaleDenominator * 0.28e-03)
845 .collect(Collectors.toList());
846 this.nativeScaleList = new ScaleList(scales);
847 }
848 this.crsScale = getTileSize() * 0.28e-03 / this.tileProjection.getMetersPerUnit();
849 }
850
851 @Override
852 public int getTileSize() {
853 if (cachedTileSize > 0) {
854 return cachedTileSize;
855 }
856 if (currentTileMatrixSet != null) {
857 // no support for non-square tiles (tileHeight != tileWidth)
858 // and for different tile sizes at different zoom levels
859 cachedTileSize = currentTileMatrixSet.tileMatrix.get(0).tileHeight;
860 return cachedTileSize;
861 }
862 // Fallback to default mercator tile size. Maybe it will work
863 Logging.warn("WMTS: Could not determine tile size. Using default tile size of: {0}", getDefaultTileSize());
864 return getDefaultTileSize();
865 }
866
867 @Override
868 public String getTileUrl(int zoom, int tilex, int tiley) {
869 if (currentLayer == null) {
870 return "";
871 }
872
873 String url;
874 if (currentLayer.baseUrl != null && transferMode == null) {
875 url = currentLayer.baseUrl;
876 } else {
877 switch (transferMode) {
878 case KVP:
879 url = baseUrl + URL_GET_ENCODING_PARAMS;
880 break;
881 case REST:
882 url = currentLayer.baseUrl;
883 break;
884 default:
885 url = "";
886 break;
887 }
888 }
889
890 TileMatrix tileMatrix = getTileMatrix(zoom);
891
892 if (tileMatrix == null) {
893 return ""; // no matrix, probably unsupported CRS selected.
894 }
895
896 url = url.replace("{layer}", this.currentLayer.identifier)
897 .replace("{format}", this.currentLayer.format)
898 .replace("{TileMatrixSet}", this.currentTileMatrixSet.identifier)
899 .replace("{TileMatrix}", tileMatrix.identifier)
900 .replace("{TileRow}", Integer.toString(tiley))
901 .replace("{TileCol}", Integer.toString(tilex))
902 .replaceAll("(?i)\\{style}", this.currentLayer.style);
903
904 for (Dimension d : currentLayer.dimensions) {
905 url = url.replaceAll("(?i)\\{"+d.identifier+"}", d.defaultValue);
906 }
907
908 return url;
909 }
910
911 /**
912 * Returns TileMatrix that's working on given zoom level
913 * @param zoom zoom level
914 * @return TileMatrix that's working on this zoom level
915 */
916 private TileMatrix getTileMatrix(int zoom) {
917 if (zoom > getMaxZoom()) {
918 return null;
919 }
920 if (zoom < 0) {
921 return null;
922 }
923 return this.currentTileMatrixSet.tileMatrix.get(zoom);
924 }
925
926 @Override
927 public double getDistance(double lat1, double lon1, double lat2, double lon2) {
928 throw new UnsupportedOperationException("Not implemented");
929 }
930
931 @Override
932 public ICoordinate tileXYToLatLon(int x, int y, int zoom) {
933 TileMatrix matrix = getTileMatrix(zoom);
934 if (matrix == null) {
935 return CoordinateConversion.llToCoor(tileProjection.getWorldBoundsLatLon().getCenter());
936 }
937 double scale = matrix.scaleDenominator * this.crsScale;
938 EastNorth ret = new EastNorth(matrix.topLeftCorner.east() + x * scale, matrix.topLeftCorner.north() - y * scale);
939 return CoordinateConversion.llToCoor(tileProjection.eastNorth2latlon(ret));
940 }
941
942 @Override
943 public TileXY latLonToTileXY(double lat, double lon, int zoom) {
944 TileMatrix matrix = getTileMatrix(zoom);
945 if (matrix == null) {
946 return new TileXY(0, 0);
947 }
948
949 EastNorth enPoint = tileProjection.latlon2eastNorth(new LatLon(lat, lon));
950 double scale = matrix.scaleDenominator * this.crsScale;
951 return new TileXY(
952 (enPoint.east() - matrix.topLeftCorner.east()) / scale,
953 (matrix.topLeftCorner.north() - enPoint.north()) / scale
954 );
955 }
956
957 @Override
958 public int getTileXMax(int zoom) {
959 return getTileXMax(zoom, tileProjection);
960 }
961
962 @Override
963 public int getTileYMax(int zoom) {
964 return getTileYMax(zoom, tileProjection);
965 }
966
967 @Override
968 public Point latLonToXY(double lat, double lon, int zoom) {
969 TileMatrix matrix = getTileMatrix(zoom);
970 if (matrix == null) {
971 return new Point(0, 0);
972 }
973 double scale = matrix.scaleDenominator * this.crsScale;
974 EastNorth point = tileProjection.latlon2eastNorth(new LatLon(lat, lon));
975 return new Point(
976 (int) Math.round((point.east() - matrix.topLeftCorner.east()) / scale),
977 (int) Math.round((matrix.topLeftCorner.north() - point.north()) / scale)
978 );
979 }
980
981 @Override
982 public Coordinate xyToLatLon(int x, int y, int zoom) {
983 TileMatrix matrix = getTileMatrix(zoom);
984 if (matrix == null) {
985 return new Coordinate(0, 0);
986 }
987 double scale = matrix.scaleDenominator * this.crsScale;
988 EastNorth ret = new EastNorth(
989 matrix.topLeftCorner.east() + x * scale,
990 matrix.topLeftCorner.north() - y * scale
991 );
992 LatLon ll = tileProjection.eastNorth2latlon(ret);
993 return new Coordinate(ll.lat(), ll.lon());
994 }
995
996 @Override
997 public Map<String, String> getHeaders() {
998 return headers;
999 }
1000
1001 @Override
1002 public int getMaxZoom() {
1003 if (this.currentTileMatrixSet != null) {
1004 return this.currentTileMatrixSet.getMaxZoom();
1005 }
1006 return 0;
1007 }
1008
1009 @Override
1010 public String getTileId(int zoom, int tilex, int tiley) {
1011 return getTileUrl(zoom, tilex, tiley);
1012 }
1013
1014 /**
1015 * Checks if url is acceptable by this Tile Source
1016 * @param url URL to check
1017 */
1018 public static void checkUrl(String url) {
1019 ImageryPatterns.checkWmtsUrlPatterns(url);
1020 }
1021
1022 /**
1023 * Group layers by name and tile matrix set.
1024 * @param layers to be grouped
1025 * @return list with entries - grouping identifier + list of layers
1026 */
1027 public static List<Entry<String, List<Layer>>> groupLayersByNameAndTileMatrixSet(Collection<Layer> layers) {
1028 Map<String, List<Layer>> layerByName = layers.stream().collect(
1029 Collectors.groupingBy(x -> x.identifier + '\u001c' + x.tileMatrixSet.identifier));
1030 return layerByName.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList());
1031 }
1032
1033 /**
1034 * Returns set of projection codes that this TileSource supports.
1035 * @return set of projection codes that this TileSource supports
1036 */
1037 public Collection<String> getSupportedProjections() {
1038 return this.layers.stream()
1039 .filter(layer -> currentLayer == null || currentLayer.identifier.equals(layer.identifier))
1040 .map(layer -> layer.tileMatrixSet.crs)
1041 .collect(Collectors.toCollection(LinkedHashSet::new));
1042 }
1043
1044 private int getTileYMax(int zoom, Projection proj) {
1045 TileMatrix matrix = getTileMatrix(zoom);
1046 if (matrix == null) {
1047 return 0;
1048 }
1049
1050 if (matrix.matrixHeight != -1) {
1051 return matrix.matrixHeight;
1052 }
1053
1054 double scale = matrix.scaleDenominator * this.crsScale;
1055 EastNorth min = matrix.topLeftCorner;
1056 EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax());
1057 return (int) Math.ceil(Math.abs(max.north() - min.north()) / scale);
1058 }
1059
1060 private int getTileXMax(int zoom, Projection proj) {
1061 TileMatrix matrix = getTileMatrix(zoom);
1062 if (matrix == null) {
1063 return 0;
1064 }
1065 if (matrix.matrixWidth != -1) {
1066 return matrix.matrixWidth;
1067 }
1068
1069 double scale = matrix.scaleDenominator * this.crsScale;
1070 EastNorth min = matrix.topLeftCorner;
1071 EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax());
1072 return (int) Math.ceil(Math.abs(max.east() - min.east()) / scale);
1073 }
1074
1075 /**
1076 * Get native scales of tile source.
1077 * @return {@link ScaleList} of native scales
1078 */
1079 public ScaleList getNativeScales() {
1080 return nativeScaleList;
1081 }
1082
1083 /**
1084 * Returns the tile projection.
1085 * @return the tile projection
1086 */
1087 public Projection getTileProjection() {
1088 return tileProjection;
1089 }
1090
1091 @Override
1092 public IProjected tileXYtoProjected(int x, int y, int zoom) {
1093 TileMatrix matrix = getTileMatrix(zoom);
1094 if (matrix == null) {
1095 return new Projected(0, 0);
1096 }
1097 double scale = matrix.scaleDenominator * this.crsScale;
1098 return new Projected(
1099 matrix.topLeftCorner.east() + x * scale,
1100 matrix.topLeftCorner.north() - y * scale);
1101 }
1102
1103 @Override
1104 public TileXY projectedToTileXY(IProjected projected, int zoom) {
1105 TileMatrix matrix = getTileMatrix(zoom);
1106 if (matrix == null) {
1107 return new TileXY(0, 0);
1108 }
1109 double scale = matrix.scaleDenominator * this.crsScale;
1110 return new TileXY(
1111 (projected.getEast() - matrix.topLeftCorner.east()) / scale,
1112 -(projected.getNorth() - matrix.topLeftCorner.north()) / scale);
1113 }
1114
1115 private EastNorth tileToEastNorth(int x, int y, int z) {
1116 return CoordinateConversion.projToEn(this.tileXYtoProjected(x, y, z));
1117 }
1118
1119 private ProjectionBounds getTileProjectionBounds(Tile tile) {
1120 ProjectionBounds pb = new ProjectionBounds(tileToEastNorth(tile.getXtile(), tile.getYtile(), tile.getZoom()));
1121 pb.extend(tileToEastNorth(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom()));
1122 return pb;
1123 }
1124
1125 @Override
1126 public boolean isInside(Tile inner, Tile outer) {
1127 ProjectionBounds pbInner = getTileProjectionBounds(inner);
1128 ProjectionBounds pbOuter = getTileProjectionBounds(outer);
1129 // a little tolerance, for when inner tile touches the border of the outer tile
1130 double epsilon = 1e-7 * (pbOuter.maxEast - pbOuter.minEast);
1131 return pbOuter.minEast <= pbInner.minEast + epsilon &&
1132 pbOuter.minNorth <= pbInner.minNorth + epsilon &&
1133 pbOuter.maxEast >= pbInner.maxEast - epsilon &&
1134 pbOuter.maxNorth >= pbInner.maxNorth - epsilon;
1135 }
1136
1137 @Override
1138 public TileRange getCoveringTileRange(Tile tile, int newZoom) {
1139 TileMatrix matrixNew = getTileMatrix(newZoom);
1140 if (matrixNew == null) {
1141 return new TileRange(new TileXY(0, 0), new TileXY(0, 0), newZoom);
1142 }
1143 IProjected p0 = tileXYtoProjected(tile.getXtile(), tile.getYtile(), tile.getZoom());
1144 IProjected p1 = tileXYtoProjected(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
1145 TileXY tMin = projectedToTileXY(p0, newZoom);
1146 TileXY tMax = projectedToTileXY(p1, newZoom);
1147 // shrink the target tile a little, so we don't get neighboring tiles, that
1148 // share an edge, but don't actually cover the target tile
1149 double epsilon = 1e-7 * (tMax.getX() - tMin.getX());
1150 int minX = (int) Math.floor(tMin.getX() + epsilon);
1151 int minY = (int) Math.floor(tMin.getY() + epsilon);
1152 int maxX = (int) Math.ceil(tMax.getX() - epsilon) - 1;
1153 int maxY = (int) Math.ceil(tMax.getY() - epsilon) - 1;
1154 return new TileRange(new TileXY(minX, minY), new TileXY(maxX, maxY), newZoom);
1155 }
1156
1157 @Override
1158 public String getServerCRS() {
1159 return tileProjection != null ? tileProjection.toCode() : null;
1160 }
1161
1162 /**
1163 * Layers that can be used with this tile source
1164 * @return unmodifiable collection of layers available in this tile source
1165 * @since 13879
1166 */
1167 public Collection<Layer> getLayers() {
1168 return Collections.unmodifiableCollection(layers);
1169 }
1170}
Note: See TracBrowser for help on using the repository browser.