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