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 * Call remote server and parse response to WMTSCapabilities object 451 * 452 * @param url of the getCapabilities document 453 * @param headers HTTP headers to set when calling getCapabilities url 454 * @return capabilities 455 * @throws IOException in case of any I/O error 456 * @throws WMTSGetCapabilitiesException when document didn't contain any layers 457 * @throws IllegalArgumentException in case of any other error 458 */ 459 public static WMTSCapabilities getCapabilities(String url, Map<String, String> headers) throws IOException, WMTSGetCapabilitiesException { 460 try (CachedFile cf = new CachedFile(url); InputStream in = cf.setHttpHeaders(headers). 461 setMaxAge(Config.getPref().getLong("wmts.capabilities.cache.max_age", 7 * CachedFile.DAYS)). 462 setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince). 463 getInputStream()) { 464 byte[] data = Utils.readBytesFromStream(in); 465 if (data.length == 0) { 466 cf.clear(); 467 throw new IllegalArgumentException("Could not read data from: " + url); 468 } 469 470 try { 471 XMLStreamReader reader = GetCapabilitiesParseHelper.getReader(new ByteArrayInputStream(data)); 472 WMTSCapabilities ret = null; 473 Collection<Layer> layers = null; 474 for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) { 475 if (event == XMLStreamReader.START_ELEMENT) { 476 QName qName = reader.getName(); 477 if (QN_OWS_OPERATIONS_METADATA.equals(qName)) { 478 ret = parseOperationMetadata(reader); 479 } else if (QN_CONTENTS.equals(qName)) { 480 layers = parseContents(reader); 481 } 482 } 483 } 484 if (ret == null) { 485 /* 486 * see #12168 - create dummy operation metadata - not all WMTS services provide this information 487 * 488 * WMTS Standard: 489 * > Resource oriented architecture style HTTP encodings SHALL not be described in the OperationsMetadata section. 490 * 491 * And OperationMetada is not mandatory element. So REST mode is justifiable 492 */ 493 ret = new WMTSCapabilities(url, TransferMode.REST); 494 } 495 if (layers == null) { 496 throw new WMTSGetCapabilitiesException(tr("WMTS Capabilities document did not contain layers in url: {0}", url)); 497 } 498 ret.addLayers(layers); 499 return ret; 500 } catch (XMLStreamException e) { 501 cf.clear(); 502 Logging.warn(new String(data, StandardCharsets.UTF_8)); 503 throw new WMTSGetCapabilitiesException(tr("Error during parsing of WMTS Capabilities document: {0}", e.getMessage()), e); 504 } 505 } catch (InvalidPathException e) { 506 throw new WMTSGetCapabilitiesException(tr("Invalid path for GetCapabilities document: {0}", e.getMessage()), e); 507 } 508 } 509 510 /** 511 * Parse Contents tag. Returns when reader reaches Contents closing tag 512 * 513 * @param reader StAX reader instance 514 * @return collection of layers within contents with properly linked TileMatrixSets 515 * @throws XMLStreamException See {@link XMLStreamReader} 516 */ 517 private static Collection<Layer> parseContents(XMLStreamReader reader) throws XMLStreamException { 518 Map<String, TileMatrixSet> matrixSetById = new ConcurrentHashMap<>(); 519 Collection<Layer> layers = new ArrayList<>(); 520 for (int event = reader.getEventType(); 521 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_CONTENTS.equals(reader.getName())); 522 event = reader.next()) { 523 if (event == XMLStreamReader.START_ELEMENT) { 524 QName qName = reader.getName(); 525 if (QN_LAYER.equals(qName)) { 526 Layer l = parseLayer(reader); 527 if (l != null) { 528 layers.add(l); 529 } 530 } else if (QN_TILEMATRIXSET.equals(qName)) { 531 TileMatrixSet entry = parseTileMatrixSet(reader); 532 matrixSetById.put(entry.identifier, entry); 533 } 534 } 535 } 536 Collection<Layer> ret = new ArrayList<>(); 537 // link layers to matrix sets 538 for (Layer l: layers) { 539 for (String tileMatrixId: l.tileMatrixSetLinks) { 540 Layer newLayer = new Layer(l); // create a new layer object for each tile matrix set supported 541 newLayer.tileMatrixSet = matrixSetById.get(tileMatrixId); 542 ret.add(newLayer); 543 } 544 } 545 return ret; 546 } 547 548 /** 549 * Parse Layer tag. Returns when reader will reach Layer closing tag 550 * 551 * @param reader StAX reader instance 552 * @return Layer object, with tileMatrixSetLinks and no tileMatrixSet attribute set. 553 * @throws XMLStreamException See {@link XMLStreamReader} 554 */ 555 private static Layer parseLayer(XMLStreamReader reader) throws XMLStreamException { 556 Layer layer = new Layer(); 557 Deque<QName> tagStack = new LinkedList<>(); 558 List<String> supportedMimeTypes = new ArrayList<>(Arrays.asList(ImageIO.getReaderMIMETypes())); 559 supportedMimeTypes.add("image/jpgpng"); // used by ESRI 560 supportedMimeTypes.add("image/png8"); // used by geoserver 561 if (supportedMimeTypes.contains("image/jpeg")) { 562 supportedMimeTypes.add("image/jpg"); // sometimes misspelled by Arcgis 563 } 564 Collection<String> unsupportedFormats = new ArrayList<>(); 565 566 for (int event = reader.getEventType(); 567 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_LAYER.equals(reader.getName())); 568 event = reader.next()) { 569 if (event == XMLStreamReader.START_ELEMENT) { 570 QName qName = reader.getName(); 571 tagStack.push(qName); 572 if (tagStack.size() == 2) { 573 if (QN_FORMAT.equals(qName)) { 574 String format = reader.getElementText(); 575 if (supportedMimeTypes.contains(format)) { 576 layer.format = format; 577 } else { 578 unsupportedFormats.add(format); 579 } 580 } else if (QN_OWS_IDENTIFIER.equals(qName)) { 581 layer.identifier = reader.getElementText(); 582 } else if (QN_OWS_TITLE.equals(qName)) { 583 layer.title = reader.getElementText(); 584 } else if (QN_RESOURCE_URL.equals(qName) && 585 "tile".equals(reader.getAttributeValue("", "resourceType"))) { 586 layer.baseUrl = reader.getAttributeValue("", "template"); 587 } else if (QN_STYLE.equals(qName) && 588 "true".equals(reader.getAttributeValue("", "isDefault"))) { 589 if (GetCapabilitiesParseHelper.moveReaderToTag(reader, QN_OWS_IDENTIFIER)) { 590 layer.style = reader.getElementText(); 591 tagStack.push(reader.getName()); // keep tagStack in sync 592 } 593 } else if (QN_DIMENSION.equals(qName)) { 594 layer.dimensions.add(parseDimension(reader)); 595 } else if (QN_TILEMATRIX_SET_LINK.equals(qName)) { 596 layer.tileMatrixSetLinks.add(parseTileMatrixSetLink(reader)); 597 } else if (QN_OWS_WGS84_BOUNDING_BOX.equals(qName)) { 598 layer.bbox = parseBoundingBox(reader); 599 } else { 600 GetCapabilitiesParseHelper.moveReaderToEndCurrentTag(reader); 601 } 602 } 603 } 604 // need to get event type from reader, as parsing might have change position of reader 605 if (reader.getEventType() == XMLStreamReader.END_ELEMENT) { 606 QName start = tagStack.pop(); 607 if (!start.equals(reader.getName())) { 608 throw new IllegalStateException(tr("WMTS Parser error - start element {0} has different name than end element {2}", 609 start, reader.getName())); 610 } 611 } 612 } 613 if (layer.style == null) { 614 layer.style = ""; 615 } 616 if (layer.format == null) { 617 // no format found - it's mandatory parameter - can't use this layer 618 Logging.warn(tr("Can''t use layer {0} because no supported formats where found. Layer is available in formats: {1}", 619 layer.getUserTitle(), 620 String.join(", ", unsupportedFormats))); 621 return null; 622 } 623 return layer; 624 } 625 626 /** 627 * Gets Dimension value. Returns when reader is on Dimension closing tag 628 * 629 * @param reader StAX reader instance 630 * @return dimension 631 * @throws XMLStreamException See {@link XMLStreamReader} 632 */ 633 private static Dimension parseDimension(XMLStreamReader reader) throws XMLStreamException { 634 Dimension ret = new Dimension(); 635 for (int event = reader.getEventType(); 636 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && 637 QN_DIMENSION.equals(reader.getName())); 638 event = reader.next()) { 639 if (event == XMLStreamReader.START_ELEMENT) { 640 QName qName = reader.getName(); 641 if (QN_OWS_IDENTIFIER.equals(qName)) { 642 ret.identifier = reader.getElementText(); 643 } else if (QN_DEFAULT.equals(qName)) { 644 ret.defaultValue = reader.getElementText(); 645 } else if (QN_VALUE.equals(qName)) { 646 ret.values.add(reader.getElementText()); 647 } 648 } 649 } 650 return ret; 651 } 652 653 /** 654 * Gets TileMatrixSetLink value. Returns when reader is on TileMatrixSetLink closing tag 655 * 656 * @param reader StAX reader instance 657 * @return TileMatrixSetLink identifier 658 * @throws XMLStreamException See {@link XMLStreamReader} 659 */ 660 private static String parseTileMatrixSetLink(XMLStreamReader reader) throws XMLStreamException { 661 String ret = null; 662 for (int event = reader.getEventType(); 663 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && 664 QN_TILEMATRIX_SET_LINK.equals(reader.getName())); 665 event = reader.next()) { 666 if (event == XMLStreamReader.START_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName())) { 667 ret = reader.getElementText(); 668 } 669 } 670 return ret; 671 } 672 673 /** 674 * Parses TileMatrixSet section. Returns when reader is on TileMatrixSet closing tag 675 * @param reader StAX reader instance 676 * @return TileMatrixSet object 677 * @throws XMLStreamException See {@link XMLStreamReader} 678 */ 679 private static TileMatrixSet parseTileMatrixSet(XMLStreamReader reader) throws XMLStreamException { 680 TileMatrixSetBuilder matrixSet = new TileMatrixSetBuilder(); 681 for (int event = reader.getEventType(); 682 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName())); 683 event = reader.next()) { 684 if (event == XMLStreamReader.START_ELEMENT) { 685 QName qName = reader.getName(); 686 if (QN_OWS_IDENTIFIER.equals(qName)) { 687 matrixSet.identifier = reader.getElementText(); 688 } else if (QN_OWS_SUPPORTED_CRS.equals(qName)) { 689 matrixSet.crs = GetCapabilitiesParseHelper.crsToCode(reader.getElementText()); 690 } else if (QN_TILEMATRIX.equals(qName)) { 691 matrixSet.tileMatrix.add(parseTileMatrix(reader, matrixSet.crs)); 692 } 693 } 694 } 695 return matrixSet.build(); 696 } 697 698 /** 699 * Parses TileMatrix section. Returns when reader is on TileMatrix closing tag. 700 * @param reader StAX reader instance 701 * @param matrixCrs projection used by this matrix 702 * @return TileMatrix object 703 * @throws XMLStreamException See {@link XMLStreamReader} 704 */ 705 private static TileMatrix parseTileMatrix(XMLStreamReader reader, String matrixCrs) throws XMLStreamException { 706 Projection matrixProj = Optional.ofNullable(Projections.getProjectionByCode(matrixCrs)) 707 .orElseGet(ProjectionRegistry::getProjection); // use current projection if none found. Maybe user is using custom string 708 TileMatrix ret = new TileMatrix(); 709 for (int event = reader.getEventType(); 710 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_TILEMATRIX.equals(reader.getName())); 711 event = reader.next()) { 712 if (event == XMLStreamReader.START_ELEMENT) { 713 QName qName = reader.getName(); 714 if (QN_OWS_IDENTIFIER.equals(qName)) { 715 ret.identifier = reader.getElementText(); 716 } else if (QN_SCALE_DENOMINATOR.equals(qName)) { 717 ret.scaleDenominator = Double.parseDouble(reader.getElementText()); 718 } else if (QN_TOPLEFT_CORNER.equals(qName)) { 719 ret.topLeftCorner = parseEastNorth(reader.getElementText(), matrixProj.switchXY()); 720 } else if (QN_TILE_HEIGHT.equals(qName)) { 721 ret.tileHeight = Integer.parseInt(reader.getElementText()); 722 } else if (QN_TILE_WIDTH.equals(qName)) { 723 ret.tileWidth = Integer.parseInt(reader.getElementText()); 724 } else if (QN_MATRIX_HEIGHT.equals(qName)) { 725 ret.matrixHeight = Integer.parseInt(reader.getElementText()); 726 } else if (QN_MATRIX_WIDTH.equals(qName)) { 727 ret.matrixWidth = Integer.parseInt(reader.getElementText()); 728 } 729 } 730 } 731 if (ret.tileHeight != ret.tileWidth) { 732 throw new AssertionError(tr("Only square tiles are supported. {0}x{1} returned by server for TileMatrix identifier {2}", 733 ret.tileHeight, ret.tileWidth, ret.identifier)); 734 } 735 return ret; 736 } 737 738 private static <T> T parseCoor(String coor, boolean switchXY, BiFunction<String, String, T> function) { 739 String[] parts = coor.split(" "); 740 if (switchXY) { 741 return function.apply(parts[1], parts[0]); 742 } else { 743 return function.apply(parts[0], parts[1]); 744 } 745 } 746 747 private static EastNorth parseEastNorth(String coor, boolean switchXY) { 748 return parseCoor(coor, switchXY, (e, n) -> new EastNorth(Double.parseDouble(e), Double.parseDouble(n))); 749 } 750 751 private static LatLon parseLatLon(String coor, boolean switchXY) { 752 return parseCoor(coor, switchXY, (lon, lat) -> new LatLon(Double.parseDouble(lat), Double.parseDouble(lon))); 753 } 754 755 /** 756 * Parses WGS84BoundingBox section. Returns when reader is on WGS84BoundingBox closing tag. 757 * @param reader StAX reader instance 758 * @return WGS84 bounding box 759 * @throws XMLStreamException See {@link XMLStreamReader} 760 */ 761 private static BBox parseBoundingBox(XMLStreamReader reader) throws XMLStreamException { 762 LatLon lowerCorner = null; 763 LatLon upperCorner = null; 764 for (int event = reader.getEventType(); 765 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && 766 QN_OWS_WGS84_BOUNDING_BOX.equals(reader.getName())); 767 event = reader.next()) { 768 if (event == XMLStreamReader.START_ELEMENT) { 769 QName qName = reader.getName(); 770 if (QN_OWS_LOWER_CORNER.equals(qName)) { 771 lowerCorner = parseLatLon(reader.getElementText(), false); 772 } else if (QN_OWS_UPPER_CORNER.equals(qName)) { 773 upperCorner = parseLatLon(reader.getElementText(), false); 774 } 775 } 776 } 777 if (lowerCorner != null && upperCorner != null) { 778 return new BBox(lowerCorner, upperCorner); 779 } 780 return null; 781 } 782 783 /** 784 * Parses OperationMetadata section. Returns when reader is on OperationsMetadata closing tag. 785 * return WMTSCapabilities with baseUrl and transferMode 786 * 787 * @param reader StAX reader instance 788 * @return WMTSCapabilities with baseUrl and transferMode set 789 * @throws XMLStreamException See {@link XMLStreamReader} 790 */ 791 private static WMTSCapabilities parseOperationMetadata(XMLStreamReader reader) throws XMLStreamException { 792 for (int event = reader.getEventType(); 793 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && 794 QN_OWS_OPERATIONS_METADATA.equals(reader.getName())); 795 event = reader.next()) { 796 if (event == XMLStreamReader.START_ELEMENT && 797 QN_OWS_OPERATION.equals(reader.getName()) && 798 "GetTile".equals(reader.getAttributeValue("", "name")) && 799 GetCapabilitiesParseHelper.moveReaderToTag(reader, QN_OWS_DCP, QN_OWS_HTTP, QN_OWS_GET)) { 800 return new WMTSCapabilities( 801 reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href"), 802 GetCapabilitiesParseHelper.getTransferMode(reader) 803 ); 804 } 805 } 806 return null; 807 } 808 809 /** 810 * Initializes projection for this TileSource with projection 811 * @param proj projection to be used by this TileSource 812 */ 813 public void initProjection(Projection proj) { 814 if (proj.equals(tileProjection)) 815 return; 816 List<Layer> matchingLayers = layers.stream().filter( 817 l -> l.identifier.equals(defaultLayer.getLayerName()) && l.tileMatrixSet.crs.equals(proj.toCode())) 818 .collect(Collectors.toList()); 819 if (matchingLayers.size() > 1) { 820 this.currentLayer = matchingLayers.stream().filter( 821 l -> l.tileMatrixSet.identifier.equals(defaultLayer.getTileMatrixSet())) 822 .findFirst().orElse(matchingLayers.get(0)); 823 this.tileProjection = proj; 824 } else if (matchingLayers.size() == 1) { 825 this.currentLayer = matchingLayers.get(0); 826 this.tileProjection = proj; 827 } else { 828 // no tile matrix sets with current projection 829 if (this.currentLayer == null) { 830 this.tileProjection = null; 831 for (Layer layer : layers) { 832 if (!layer.identifier.equals(defaultLayer.getLayerName())) { 833 continue; 834 } 835 Projection pr = Projections.getProjectionByCode(layer.tileMatrixSet.crs); 836 if (pr != null) { 837 this.currentLayer = layer; 838 this.tileProjection = pr; 839 break; 840 } 841 } 842 if (this.currentLayer == null) 843 throw new IllegalArgumentException( 844 layers.stream().map(l -> l.tileMatrixSet).collect(Collectors.toList()).toString()); 845 } // else: keep currentLayer and tileProjection as is 846 } 847 if (this.currentLayer != null) { 848 this.currentTileMatrixSet = this.currentLayer.tileMatrixSet; 849 Collection<Double> scales = new ArrayList<>(currentTileMatrixSet.tileMatrix.size()); 850 for (TileMatrix tileMatrix : currentTileMatrixSet.tileMatrix) { 851 scales.add(tileMatrix.scaleDenominator * 0.28e-03); 852 } 853 this.nativeScaleList = new ScaleList(scales); 854 } 855 this.crsScale = getTileSize() * 0.28e-03 / this.tileProjection.getMetersPerUnit(); 856 } 857 858 @Override 859 public int getTileSize() { 860 if (cachedTileSize > 0) { 861 return cachedTileSize; 862 } 863 if (currentTileMatrixSet != null) { 864 // no support for non-square tiles (tileHeight != tileWidth) 865 // and for different tile sizes at different zoom levels 866 cachedTileSize = currentTileMatrixSet.tileMatrix.get(0).tileHeight; 867 return cachedTileSize; 868 } 869 // Fallback to default mercator tile size. Maybe it will work 870 Logging.warn("WMTS: Could not determine tile size. Using default tile size of: {0}", getDefaultTileSize()); 871 return getDefaultTileSize(); 872 } 873 874 @Override 875 public String getTileUrl(int zoom, int tilex, int tiley) { 876 if (currentLayer == null) { 877 return ""; 878 } 879 880 String url; 881 if (currentLayer.baseUrl != null && transferMode == null) { 882 url = currentLayer.baseUrl; 883 } else { 884 switch (transferMode) { 885 case KVP: 886 url = baseUrl + URL_GET_ENCODING_PARAMS; 887 break; 888 case REST: 889 url = currentLayer.baseUrl; 890 break; 891 default: 892 url = ""; 893 break; 894 } 895 } 896 897 TileMatrix tileMatrix = getTileMatrix(zoom); 898 899 if (tileMatrix == null) { 900 return ""; // no matrix, probably unsupported CRS selected. 901 } 902 903 url = url.replaceAll("\\{layer\\}", this.currentLayer.identifier) 904 .replaceAll("\\{format\\}", this.currentLayer.format) 905 .replaceAll("\\{TileMatrixSet\\}", this.currentTileMatrixSet.identifier) 906 .replaceAll("\\{TileMatrix\\}", tileMatrix.identifier) 907 .replaceAll("\\{TileRow\\}", Integer.toString(tiley)) 908 .replaceAll("\\{TileCol\\}", Integer.toString(tilex)) 909 .replaceAll("(?i)\\{style\\}", this.currentLayer.style); 910 911 for (Dimension d : currentLayer.dimensions) { 912 url = url.replaceAll("(?i)\\{"+d.identifier+"\\}", d.defaultValue); 913 } 914 915 return url; 916 } 917 918 /** 919 * 920 * @param zoom zoom level 921 * @return TileMatrix that's working on this zoom level 922 */ 923 private TileMatrix getTileMatrix(int zoom) { 924 if (zoom > getMaxZoom()) { 925 return null; 926 } 927 if (zoom < 0) { 928 return null; 929 } 930 return this.currentTileMatrixSet.tileMatrix.get(zoom); 931 } 932 933 @Override 934 public double getDistance(double lat1, double lon1, double lat2, double lon2) { 935 throw new UnsupportedOperationException("Not implemented"); 936 } 937 938 @Override 939 public ICoordinate tileXYToLatLon(Tile tile) { 940 return tileXYToLatLon(tile.getXtile(), tile.getYtile(), tile.getZoom()); 941 } 942 943 @Override 944 public ICoordinate tileXYToLatLon(TileXY xy, int zoom) { 945 return tileXYToLatLon(xy.getXIndex(), xy.getYIndex(), zoom); 946 } 947 948 @Override 949 public ICoordinate tileXYToLatLon(int x, int y, int zoom) { 950 TileMatrix matrix = getTileMatrix(zoom); 951 if (matrix == null) { 952 return CoordinateConversion.llToCoor(tileProjection.getWorldBoundsLatLon().getCenter()); 953 } 954 double scale = matrix.scaleDenominator * this.crsScale; 955 EastNorth ret = new EastNorth(matrix.topLeftCorner.east() + x * scale, matrix.topLeftCorner.north() - y * scale); 956 return CoordinateConversion.llToCoor(tileProjection.eastNorth2latlon(ret)); 957 } 958 959 @Override 960 public TileXY latLonToTileXY(double lat, double lon, int zoom) { 961 TileMatrix matrix = getTileMatrix(zoom); 962 if (matrix == null) { 963 return new TileXY(0, 0); 964 } 965 966 EastNorth enPoint = tileProjection.latlon2eastNorth(new LatLon(lat, lon)); 967 double scale = matrix.scaleDenominator * this.crsScale; 968 return new TileXY( 969 (enPoint.east() - matrix.topLeftCorner.east()) / scale, 970 (matrix.topLeftCorner.north() - enPoint.north()) / scale 971 ); 972 } 973 974 @Override 975 public TileXY latLonToTileXY(ICoordinate point, int zoom) { 976 return latLonToTileXY(point.getLat(), point.getLon(), zoom); 977 } 978 979 @Override 980 public int getTileXMax(int zoom) { 981 return getTileXMax(zoom, tileProjection); 982 } 983 984 @Override 985 public int getTileYMax(int zoom) { 986 return getTileYMax(zoom, tileProjection); 987 } 988 989 @Override 990 public Point latLonToXY(double lat, double lon, int zoom) { 991 TileMatrix matrix = getTileMatrix(zoom); 992 if (matrix == null) { 993 return new Point(0, 0); 994 } 995 double scale = matrix.scaleDenominator * this.crsScale; 996 EastNorth point = tileProjection.latlon2eastNorth(new LatLon(lat, lon)); 997 return new Point( 998 (int) Math.round((point.east() - matrix.topLeftCorner.east()) / scale), 999 (int) Math.round((matrix.topLeftCorner.north() - point.north()) / scale) 1000 ); 1001 } 1002 1003 @Override 1004 public Point latLonToXY(ICoordinate point, int zoom) { 1005 return latLonToXY(point.getLat(), point.getLon(), zoom); 1006 } 1007 1008 @Override 1009 public Coordinate xyToLatLon(Point point, int zoom) { 1010 return xyToLatLon(point.x, point.y, zoom); 1011 } 1012 1013 @Override 1014 public Coordinate xyToLatLon(int x, int y, int zoom) { 1015 TileMatrix matrix = getTileMatrix(zoom); 1016 if (matrix == null) { 1017 return new Coordinate(0, 0); 1018 } 1019 double scale = matrix.scaleDenominator * this.crsScale; 1020 EastNorth ret = new EastNorth( 1021 matrix.topLeftCorner.east() + x * scale, 1022 matrix.topLeftCorner.north() - y * scale 1023 ); 1024 LatLon ll = tileProjection.eastNorth2latlon(ret); 1025 return new Coordinate(ll.lat(), ll.lon()); 1026 } 1027 1028 @Override 1029 public Map<String, String> getHeaders() { 1030 return headers; 1031 } 1032 1033 @Override 1034 public int getMaxZoom() { 1035 if (this.currentTileMatrixSet != null) { 1036 return this.currentTileMatrixSet.getMaxZoom(); 1037 } 1038 return 0; 1039 } 1040 1041 @Override 1042 public String getTileId(int zoom, int tilex, int tiley) { 1043 return getTileUrl(zoom, tilex, tiley); 1044 } 1045 1046 /** 1047 * Checks if url is acceptable by this Tile Source 1048 * @param url URL to check 1049 */ 1050 public static void checkUrl(String url) { 1051 CheckParameterUtil.ensureParameterNotNull(url, "url"); 1052 Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url); 1053 while (m.find()) { 1054 boolean isSupportedPattern = false; 1055 for (String pattern : ALL_PATTERNS) { 1056 if (m.group().matches(pattern)) { 1057 isSupportedPattern = true; 1058 break; 1059 } 1060 } 1061 if (!isSupportedPattern) { 1062 throw new IllegalArgumentException( 1063 tr("{0} is not a valid WMS argument. Please check this server URL:\n{1}", m.group(), url)); 1064 } 1065 } 1066 } 1067 1068 /** 1069 * @param layers to be grouped 1070 * @return list with entries - grouping identifier + list of layers 1071 */ 1072 public static List<Entry<String, List<Layer>>> groupLayersByNameAndTileMatrixSet(Collection<Layer> layers) { 1073 Map<String, List<Layer>> layerByName = layers.stream().collect( 1074 Collectors.groupingBy(x -> x.identifier + '\u001c' + x.tileMatrixSet.identifier)); 1075 return layerByName.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList()); 1076 } 1077 1078 /** 1079 * @return set of projection codes that this TileSource supports 1080 */ 1081 public Collection<String> getSupportedProjections() { 1082 Collection<String> ret = new LinkedHashSet<>(); 1083 if (currentLayer == null) { 1084 for (Layer layer: this.layers) { 1085 ret.add(layer.tileMatrixSet.crs); 1086 } 1087 } else { 1088 for (Layer layer: this.layers) { 1089 if (currentLayer.identifier.equals(layer.identifier)) { 1090 ret.add(layer.tileMatrixSet.crs); 1091 } 1092 } 1093 } 1094 return ret; 1095 } 1096 1097 private int getTileYMax(int zoom, Projection proj) { 1098 TileMatrix matrix = getTileMatrix(zoom); 1099 if (matrix == null) { 1100 return 0; 1101 } 1102 1103 if (matrix.matrixHeight != -1) { 1104 return matrix.matrixHeight; 1105 } 1106 1107 double scale = matrix.scaleDenominator * this.crsScale; 1108 EastNorth min = matrix.topLeftCorner; 1109 EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax()); 1110 return (int) Math.ceil(Math.abs(max.north() - min.north()) / scale); 1111 } 1112 1113 private int getTileXMax(int zoom, Projection proj) { 1114 TileMatrix matrix = getTileMatrix(zoom); 1115 if (matrix == null) { 1116 return 0; 1117 } 1118 if (matrix.matrixWidth != -1) { 1119 return matrix.matrixWidth; 1120 } 1121 1122 double scale = matrix.scaleDenominator * this.crsScale; 1123 EastNorth min = matrix.topLeftCorner; 1124 EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax()); 1125 return (int) Math.ceil(Math.abs(max.east() - min.east()) / scale); 1126 } 1127 1128 /** 1129 * Get native scales of tile source. 1130 * @return {@link ScaleList} of native scales 1131 */ 1132 public ScaleList getNativeScales() { 1133 return nativeScaleList; 1134 } 1135 1136 /** 1137 * Returns the tile projection. 1138 * @return the tile projection 1139 */ 1140 public Projection getTileProjection() { 1141 return tileProjection; 1142 } 1143 1144 @Override 1145 public IProjected tileXYtoProjected(int x, int y, int zoom) { 1146 TileMatrix matrix = getTileMatrix(zoom); 1147 if (matrix == null) { 1148 return new Projected(0, 0); 1149 } 1150 double scale = matrix.scaleDenominator * this.crsScale; 1151 return new Projected( 1152 matrix.topLeftCorner.east() + x * scale, 1153 matrix.topLeftCorner.north() - y * scale); 1154 } 1155 1156 @Override 1157 public TileXY projectedToTileXY(IProjected projected, int zoom) { 1158 TileMatrix matrix = getTileMatrix(zoom); 1159 if (matrix == null) { 1160 return new TileXY(0, 0); 1161 } 1162 double scale = matrix.scaleDenominator * this.crsScale; 1163 return new TileXY( 1164 (projected.getEast() - matrix.topLeftCorner.east()) / scale, 1165 -(projected.getNorth() - matrix.topLeftCorner.north()) / scale); 1166 } 1167 1168 private EastNorth tileToEastNorth(int x, int y, int z) { 1169 return CoordinateConversion.projToEn(this.tileXYtoProjected(x, y, z)); 1170 } 1171 1172 private ProjectionBounds getTileProjectionBounds(Tile tile) { 1173 ProjectionBounds pb = new ProjectionBounds(tileToEastNorth(tile.getXtile(), tile.getYtile(), tile.getZoom())); 1174 pb.extend(tileToEastNorth(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom())); 1175 return pb; 1176 } 1177 1178 @Override 1179 public boolean isInside(Tile inner, Tile outer) { 1180 ProjectionBounds pbInner = getTileProjectionBounds(inner); 1181 ProjectionBounds pbOuter = getTileProjectionBounds(outer); 1182 // a little tolerance, for when inner tile touches the border of the outer tile 1183 double epsilon = 1e-7 * (pbOuter.maxEast - pbOuter.minEast); 1184 return pbOuter.minEast <= pbInner.minEast + epsilon && 1185 pbOuter.minNorth <= pbInner.minNorth + epsilon && 1186 pbOuter.maxEast >= pbInner.maxEast - epsilon && 1187 pbOuter.maxNorth >= pbInner.maxNorth - epsilon; 1188 } 1189 1190 @Override 1191 public TileRange getCoveringTileRange(Tile tile, int newZoom) { 1192 TileMatrix matrixNew = getTileMatrix(newZoom); 1193 if (matrixNew == null) { 1194 return new TileRange(new TileXY(0, 0), new TileXY(0, 0), newZoom); 1195 } 1196 IProjected p0 = tileXYtoProjected(tile.getXtile(), tile.getYtile(), tile.getZoom()); 1197 IProjected p1 = tileXYtoProjected(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom()); 1198 TileXY tMin = projectedToTileXY(p0, newZoom); 1199 TileXY tMax = projectedToTileXY(p1, newZoom); 1200 // shrink the target tile a little, so we don't get neighboring tiles, that 1201 // share an edge, but don't actually cover the target tile 1202 double epsilon = 1e-7 * (tMax.getX() - tMin.getX()); 1203 int minX = (int) Math.floor(tMin.getX() + epsilon); 1204 int minY = (int) Math.floor(tMin.getY() + epsilon); 1205 int maxX = (int) Math.ceil(tMax.getX() - epsilon) - 1; 1206 int maxY = (int) Math.ceil(tMax.getY() - epsilon) - 1; 1207 return new TileRange(new TileXY(minX, minY), new TileXY(maxX, maxY), newZoom); 1208 } 1209 1210 @Override 1211 public String getServerCRS() { 1212 return tileProjection != null ? tileProjection.toCode() : null; 1213 } 1214 1215 /** 1216 * Layers that can be used with this tile source 1217 * @return unmodifiable collection of layers available in this tile source 1218 * @since 13879 1219 */ 1220 public Collection<Layer> getLayers() { 1221 return Collections.unmodifiableCollection(layers); 1222 } 1223}