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, 15 months 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.