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