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

Last change on this file since 16553 was 16553, checked in by Don-vip, 4 years ago

see #19334 - javadoc fixes + protected constructors for abstract classes

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