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.GridBagLayout; 007import java.awt.Point; 008import java.io.ByteArrayInputStream; 009import java.io.IOException; 010import java.io.InputStream; 011import java.nio.charset.StandardCharsets; 012import java.util.ArrayList; 013import java.util.Collection; 014import java.util.Collections; 015import java.util.HashSet; 016import java.util.List; 017import java.util.Map; 018import java.util.Map.Entry; 019import java.util.Set; 020import java.util.SortedSet; 021import java.util.Stack; 022import java.util.TreeSet; 023import java.util.concurrent.ConcurrentHashMap; 024import java.util.regex.Matcher; 025import java.util.regex.Pattern; 026import java.util.stream.Collectors; 027 028import javax.swing.JPanel; 029import javax.swing.JScrollPane; 030import javax.swing.JTable; 031import javax.swing.ListSelectionModel; 032import javax.swing.table.AbstractTableModel; 033import javax.xml.namespace.QName; 034import javax.xml.stream.XMLStreamException; 035import javax.xml.stream.XMLStreamReader; 036 037import org.openstreetmap.gui.jmapviewer.Coordinate; 038import org.openstreetmap.gui.jmapviewer.Tile; 039import org.openstreetmap.gui.jmapviewer.TileXY; 040import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; 041import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource; 042import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource; 043import org.openstreetmap.josm.Main; 044import org.openstreetmap.josm.data.coor.EastNorth; 045import org.openstreetmap.josm.data.coor.LatLon; 046import org.openstreetmap.josm.data.projection.Projection; 047import org.openstreetmap.josm.data.projection.Projections; 048import org.openstreetmap.josm.gui.ExtendedDialog; 049import org.openstreetmap.josm.gui.layer.NativeScaleLayer.ScaleList; 050import org.openstreetmap.josm.io.CachedFile; 051import org.openstreetmap.josm.tools.CheckParameterUtil; 052import org.openstreetmap.josm.tools.GBC; 053import org.openstreetmap.josm.tools.Utils; 054 055/** 056 * Tile Source handling WMS providers 057 * 058 * @author Wiktor Niesiobędzki 059 * @since 8526 060 */ 061public class WMTSTileSource extends AbstractTMSTileSource implements TemplatedTileSource { 062 /** 063 * WMTS namespace address 064 */ 065 public static final String WMTS_NS_URL = "http://www.opengis.net/wmts/1.0"; 066 067 // CHECKSTYLE.OFF: SingleSpaceSeparator 068 private static final QName QN_CONTENTS = new QName(WMTSTileSource.WMTS_NS_URL, "Contents"); 069 private static final QName QN_FORMAT = new QName(WMTSTileSource.WMTS_NS_URL, "Format"); 070 private static final QName QN_LAYER = new QName(WMTSTileSource.WMTS_NS_URL, "Layer"); 071 private static final QName QN_MATRIX_WIDTH = new QName(WMTSTileSource.WMTS_NS_URL, "MatrixWidth"); 072 private static final QName QN_MATRIX_HEIGHT = new QName(WMTSTileSource.WMTS_NS_URL, "MatrixHeight"); 073 private static final QName QN_RESOURCE_URL = new QName(WMTSTileSource.WMTS_NS_URL, "ResourceURL"); 074 private static final QName QN_SCALE_DENOMINATOR = new QName(WMTSTileSource.WMTS_NS_URL, "ScaleDenominator"); 075 private static final QName QN_STYLE = new QName(WMTSTileSource.WMTS_NS_URL, "Style"); 076 private static final QName QN_TILEMATRIX = new QName(WMTSTileSource.WMTS_NS_URL, "TileMatrix"); 077 private static final QName QN_TILEMATRIXSET = new QName(WMTSTileSource.WMTS_NS_URL, "TileMatrixSet"); 078 private static final QName QN_TILEMATRIX_SET_LINK = new QName(WMTSTileSource.WMTS_NS_URL, "TileMatrixSetLink"); 079 private static final QName QN_TILE_WIDTH = new QName(WMTSTileSource.WMTS_NS_URL, "TileWidth"); 080 private static final QName QN_TILE_HEIGHT = new QName(WMTSTileSource.WMTS_NS_URL, "TileHeight"); 081 private static final QName QN_TOPLEFT_CORNER = new QName(WMTSTileSource.WMTS_NS_URL, "TopLeftCorner"); 082 // CHECKSTYLE.ON: SingleSpaceSeparator 083 084 private static final String PATTERN_HEADER = "\\{header\\(([^,]+),([^}]+)\\)\\}"; 085 086 private static final String URL_GET_ENCODING_PARAMS = "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER={layer}&STYLE={style}&" 087 + "FORMAT={format}&tileMatrixSet={TileMatrixSet}&tileMatrix={TileMatrix}&tileRow={TileRow}&tileCol={TileCol}"; 088 089 private static final String[] ALL_PATTERNS = { 090 PATTERN_HEADER, 091 }; 092 093 private static class TileMatrix { 094 private String identifier; 095 private double scaleDenominator; 096 private EastNorth topLeftCorner; 097 private int tileWidth; 098 private int tileHeight; 099 private int matrixWidth = -1; 100 private int matrixHeight = -1; 101 } 102 103 private static class TileMatrixSetBuilder { 104 // sorted by zoom level 105 SortedSet<TileMatrix> tileMatrix = new TreeSet<>((o1, o2) -> -1 * Double.compare(o1.scaleDenominator, o2.scaleDenominator)); 106 private String crs; 107 private String identifier; 108 109 TileMatrixSet build() { 110 return new TileMatrixSet(this); 111 } 112 } 113 114 private static class TileMatrixSet { 115 116 private final List<TileMatrix> tileMatrix; 117 private final String crs; 118 private final String identifier; 119 120 TileMatrixSet(TileMatrixSet tileMatrixSet) { 121 if (tileMatrixSet != null) { 122 tileMatrix = new ArrayList<>(tileMatrixSet.tileMatrix); 123 crs = tileMatrixSet.crs; 124 identifier = tileMatrixSet.identifier; 125 } else { 126 tileMatrix = Collections.emptyList(); 127 crs = null; 128 identifier = null; 129 } 130 } 131 132 TileMatrixSet(TileMatrixSetBuilder builder) { 133 tileMatrix = new ArrayList<>(builder.tileMatrix); 134 crs = builder.crs; 135 identifier = builder.identifier; 136 } 137 138 } 139 140 private static class Layer { 141 private String format; 142 private String name; 143 private TileMatrixSet tileMatrixSet; 144 private String baseUrl; 145 private String style; 146 private final Collection<String> tileMatrixSetLinks = new ArrayList<>(); 147 148 Layer(Layer l) { 149 if (l != null) { 150 format = l.format; 151 name = l.name; 152 baseUrl = l.baseUrl; 153 style = l.style; 154 tileMatrixSet = new TileMatrixSet(l.tileMatrixSet); 155 } 156 } 157 158 Layer() { 159 } 160 } 161 162 private static final class SelectLayerDialog extends ExtendedDialog { 163 private final transient List<Entry<String, List<Layer>>> layers; 164 private final JTable list; 165 166 SelectLayerDialog(Collection<Layer> layers) { 167 super(Main.parent, tr("Select WMTS layer"), new String[]{tr("Add layers"), tr("Cancel")}); 168 this.layers = groupLayersByName(layers); 169 //getLayersTable(layers, Main.getProjection()) 170 this.list = new JTable( 171 new AbstractTableModel() { 172 @Override 173 public Object getValueAt(int rowIndex, int columnIndex) { 174 switch (columnIndex) { 175 case 0: 176 return SelectLayerDialog.this.layers.get(rowIndex).getKey(); 177 case 1: 178 return SelectLayerDialog.this.layers.get(rowIndex).getValue() 179 .stream() 180 .map(x -> x.tileMatrixSet.crs) 181 .collect(Collectors.joining(", ")); 182 case 2: 183 return SelectLayerDialog.this.layers.get(rowIndex).getValue() 184 .stream() 185 .map(x -> x.tileMatrixSet.identifier) 186 .collect(Collectors.joining(", ")); 187 default: 188 throw new IllegalArgumentException(); 189 } 190 } 191 192 @Override 193 public int getRowCount() { 194 return SelectLayerDialog.this.layers.size(); 195 } 196 197 @Override 198 public int getColumnCount() { 199 return 3; 200 } 201 202 @Override 203 public String getColumnName(int column) { 204 switch (column) { 205 case 0: return tr("Layer name"); 206 case 1: return tr("Projection"); 207 case 2: return tr("Matrix set identifier"); 208 default: 209 throw new IllegalArgumentException(); 210 } 211 } 212 213 @Override 214 public boolean isCellEditable(int row, int column) { 215 return false; 216 } 217 }); 218 this.list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 219 this.list.setRowSelectionAllowed(true); 220 this.list.setColumnSelectionAllowed(false); 221 JPanel panel = new JPanel(new GridBagLayout()); 222 panel.add(new JScrollPane(this.list), GBC.eol().fill()); 223 setContent(panel); 224 } 225 226 private static List<Entry<String, List<Layer>>> groupLayersByName(Collection<Layer> layers) { 227 Map<String, List<Layer>> layerByName = layers.stream().collect(Collectors.groupingBy(x -> x.name)); 228 return layerByName.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList()); 229 } 230 231 public String getSelectedLayer() { 232 int index = list.getSelectedRow(); 233 if (index < 0) { 234 return null; //nothing selected 235 } 236 return layers.get(index).getKey(); 237 } 238 } 239 240 private final Map<String, String> headers = new ConcurrentHashMap<>(); 241 private final Collection<Layer> layers; 242 private Layer currentLayer; 243 private TileMatrixSet currentTileMatrixSet; 244 private double crsScale; 245 private GetCapabilitiesParseHelper.TransferMode transferMode; 246 247 private ScaleList nativeScaleList; 248 249 private final String defaultLayer; 250 251 /** 252 * Creates a tile source based on imagery info 253 * @param info imagery info 254 * @throws IOException if any I/O error occurs 255 * @throws IllegalArgumentException if any other error happens for the given imagery info 256 */ 257 public WMTSTileSource(ImageryInfo info) throws IOException { 258 super(info); 259 CheckParameterUtil.ensureThat(info.getDefaultLayers().size() < 2, "At most 1 default layer for WMTS is supported"); 260 261 this.baseUrl = GetCapabilitiesParseHelper.normalizeCapabilitiesUrl(handleTemplate(info.getUrl())); 262 this.layers = getCapabilities(); 263 this.defaultLayer = info.getDefaultLayers().isEmpty() ? null : info.getDefaultLayers().iterator().next(); 264 if (this.layers.isEmpty()) 265 throw new IllegalArgumentException(tr("No layers defined by getCapabilities document: {0}", info.getUrl())); 266 } 267 268 /** 269 * Creates a dialog based on this tile source with all available layers and returns the name of selected layer 270 * @return Name of selected layer 271 */ 272 public String userSelectLayer() { 273 Collection<String> layerNames = layers.stream().map(x -> x.name).collect(Collectors.toSet()); 274 275 // if there is only one layer name no point in asking 276 if (layerNames.size() == 1) 277 return layerNames.iterator().next(); 278 279 final SelectLayerDialog layerSelection = new SelectLayerDialog(layers); 280 if (layerSelection.showDialog().getValue() == 1) { 281 return layerSelection.getSelectedLayer(); 282 } 283 return null; 284 } 285 286 private String handleTemplate(String url) { 287 Pattern pattern = Pattern.compile(PATTERN_HEADER); 288 StringBuffer output = new StringBuffer(); 289 Matcher matcher = pattern.matcher(url); 290 while (matcher.find()) { 291 this.headers.put(matcher.group(1), matcher.group(2)); 292 matcher.appendReplacement(output, ""); 293 } 294 matcher.appendTail(output); 295 return output.toString(); 296 } 297 298 /** 299 * @return capabilities 300 * @throws IOException in case of any I/O error 301 * @throws IllegalArgumentException in case of any other error 302 */ 303 private Collection<Layer> getCapabilities() throws IOException { 304 try (CachedFile cf = new CachedFile(baseUrl); InputStream in = cf.setHttpHeaders(headers). 305 setMaxAge(7 * CachedFile.DAYS). 306 setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince). 307 getInputStream()) { 308 byte[] data = Utils.readBytesFromStream(in); 309 if (data == null || data.length == 0) { 310 cf.clear(); 311 throw new IllegalArgumentException("Could not read data from: " + baseUrl); 312 } 313 314 try { 315 XMLStreamReader reader = GetCapabilitiesParseHelper.getReader(new ByteArrayInputStream(data)); 316 Collection<Layer> ret = new ArrayList<>(); 317 for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) { 318 if (event == XMLStreamReader.START_ELEMENT) { 319 if (GetCapabilitiesParseHelper.QN_OWS_OPERATIONS_METADATA.equals(reader.getName())) { 320 parseOperationMetadata(reader); 321 } 322 323 if (QN_CONTENTS.equals(reader.getName())) { 324 ret = parseContents(reader); 325 } 326 } 327 } 328 return ret; 329 } catch (XMLStreamException e) { 330 cf.clear(); 331 Main.warn(new String(data, StandardCharsets.UTF_8)); 332 throw new IllegalArgumentException(e); 333 } 334 } 335 } 336 337 /** 338 * Parse Contents tag. Returns when reader reaches Contents closing tag 339 * 340 * @param reader StAX reader instance 341 * @return collection of layers within contents with properly linked TileMatrixSets 342 * @throws XMLStreamException See {@link XMLStreamReader} 343 */ 344 private static Collection<Layer> parseContents(XMLStreamReader reader) throws XMLStreamException { 345 Map<String, TileMatrixSet> matrixSetById = new ConcurrentHashMap<>(); 346 Collection<Layer> layers = new ArrayList<>(); 347 for (int event = reader.getEventType(); 348 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_CONTENTS.equals(reader.getName())); 349 event = reader.next()) { 350 if (event == XMLStreamReader.START_ELEMENT) { 351 if (QN_LAYER.equals(reader.getName())) { 352 layers.add(parseLayer(reader)); 353 } 354 if (QN_TILEMATRIXSET.equals(reader.getName())) { 355 TileMatrixSet entry = parseTileMatrixSet(reader); 356 matrixSetById.put(entry.identifier, entry); 357 } 358 } 359 } 360 Collection<Layer> ret = new ArrayList<>(); 361 // link layers to matrix sets 362 for (Layer l: layers) { 363 for (String tileMatrixId: l.tileMatrixSetLinks) { 364 Layer newLayer = new Layer(l); // create a new layer object for each tile matrix set supported 365 newLayer.tileMatrixSet = matrixSetById.get(tileMatrixId); 366 ret.add(newLayer); 367 } 368 } 369 return ret; 370 } 371 372 /** 373 * Parse Layer tag. Returns when reader will reach Layer closing tag 374 * 375 * @param reader StAX reader instance 376 * @return Layer object, with tileMatrixSetLinks and no tileMatrixSet attribute set. 377 * @throws XMLStreamException See {@link XMLStreamReader} 378 */ 379 private static Layer parseLayer(XMLStreamReader reader) throws XMLStreamException { 380 Layer layer = new Layer(); 381 Stack<QName> tagStack = new Stack<>(); 382 383 for (int event = reader.getEventType(); 384 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_LAYER.equals(reader.getName())); 385 event = reader.next()) { 386 if (event == XMLStreamReader.START_ELEMENT) { 387 tagStack.push(reader.getName()); 388 if (tagStack.size() == 2) { 389 if (QN_FORMAT.equals(reader.getName())) { 390 layer.format = reader.getElementText(); 391 } else if (GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER.equals(reader.getName())) { 392 layer.name = reader.getElementText(); 393 } else if (QN_RESOURCE_URL.equals(reader.getName()) && 394 "tile".equals(reader.getAttributeValue("", "resourceType"))) { 395 layer.baseUrl = reader.getAttributeValue("", "template"); 396 } else if (QN_STYLE.equals(reader.getName()) && 397 "true".equals(reader.getAttributeValue("", "isDefault"))) { 398 if (GetCapabilitiesParseHelper.moveReaderToTag(reader, new QName[] {GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER})) { 399 layer.style = reader.getElementText(); 400 tagStack.push(reader.getName()); // keep tagStack in sync 401 } 402 } else if (QN_TILEMATRIX_SET_LINK.equals(reader.getName())) { 403 layer.tileMatrixSetLinks.add(praseTileMatrixSetLink(reader)); 404 } else { 405 GetCapabilitiesParseHelper.moveReaderToEndCurrentTag(reader); 406 } 407 } 408 } 409 // need to get event type from reader, as parsing might have change position of reader 410 if (reader.getEventType() == XMLStreamReader.END_ELEMENT) { 411 QName start = tagStack.pop(); 412 if (!start.equals(reader.getName())) { 413 throw new IllegalStateException(tr("WMTS Parser error - start element {0} has different name than end element {2}", 414 start, reader.getName())); 415 } 416 } 417 } 418 if (layer.style == null) { 419 layer.style = ""; 420 } 421 return layer; 422 } 423 424 /** 425 * Gets TileMatrixSetLink value. Returns when reader is on TileMatrixSetLink closing tag 426 * 427 * @param reader StAX reader instance 428 * @return TileMatrixSetLink identifier 429 * @throws XMLStreamException See {@link XMLStreamReader} 430 */ 431 private static String praseTileMatrixSetLink(XMLStreamReader reader) throws XMLStreamException { 432 String ret = null; 433 for (int event = reader.getEventType(); 434 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && 435 QN_TILEMATRIX_SET_LINK.equals(reader.getName())); 436 event = reader.next()) { 437 if (event == XMLStreamReader.START_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName())) { 438 ret = reader.getElementText(); 439 } 440 } 441 return ret; 442 } 443 444 /** 445 * Parses TileMatrixSet section. Returns when reader is on TileMatrixSet closing tag 446 * @param reader StAX reader instance 447 * @return TileMatrixSet object 448 * @throws XMLStreamException See {@link XMLStreamReader} 449 */ 450 private static TileMatrixSet parseTileMatrixSet(XMLStreamReader reader) throws XMLStreamException { 451 TileMatrixSetBuilder matrixSet = new TileMatrixSetBuilder(); 452 for (int event = reader.getEventType(); 453 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName())); 454 event = reader.next()) { 455 if (event == XMLStreamReader.START_ELEMENT) { 456 if (GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER.equals(reader.getName())) { 457 matrixSet.identifier = reader.getElementText(); 458 } 459 if (GetCapabilitiesParseHelper.QN_OWS_SUPPORTED_CRS.equals(reader.getName())) { 460 matrixSet.crs = GetCapabilitiesParseHelper.crsToCode(reader.getElementText()); 461 } 462 if (QN_TILEMATRIX.equals(reader.getName())) { 463 matrixSet.tileMatrix.add(parseTileMatrix(reader, matrixSet.crs)); 464 } 465 } 466 } 467 return matrixSet.build(); 468 } 469 470 /** 471 * Parses TileMatrix section. Returns when reader is on TileMatrix closing tag. 472 * @param reader StAX reader instance 473 * @param matrixCrs projection used by this matrix 474 * @return TileMatrix object 475 * @throws XMLStreamException See {@link XMLStreamReader} 476 */ 477 private static TileMatrix parseTileMatrix(XMLStreamReader reader, String matrixCrs) throws XMLStreamException { 478 Projection matrixProj = Projections.getProjectionByCode(matrixCrs); 479 TileMatrix ret = new TileMatrix(); 480 481 if (matrixProj == null) { 482 // use current projection if none found. Maybe user is using custom string 483 matrixProj = Main.getProjection(); 484 } 485 for (int event = reader.getEventType(); 486 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_TILEMATRIX.equals(reader.getName())); 487 event = reader.next()) { 488 if (event == XMLStreamReader.START_ELEMENT) { 489 if (GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER.equals(reader.getName())) { 490 ret.identifier = reader.getElementText(); 491 } 492 if (QN_SCALE_DENOMINATOR.equals(reader.getName())) { 493 ret.scaleDenominator = Double.parseDouble(reader.getElementText()); 494 } 495 if (QN_TOPLEFT_CORNER.equals(reader.getName())) { 496 String[] topLeftCorner = reader.getElementText().split(" "); 497 if (matrixProj.switchXY()) { 498 ret.topLeftCorner = new EastNorth(Double.parseDouble(topLeftCorner[1]), Double.parseDouble(topLeftCorner[0])); 499 } else { 500 ret.topLeftCorner = new EastNorth(Double.parseDouble(topLeftCorner[0]), Double.parseDouble(topLeftCorner[1])); 501 } 502 } 503 if (QN_TILE_HEIGHT.equals(reader.getName())) { 504 ret.tileHeight = Integer.parseInt(reader.getElementText()); 505 } 506 if (QN_TILE_WIDTH.equals(reader.getName())) { 507 ret.tileWidth = Integer.parseInt(reader.getElementText()); 508 } 509 if (QN_MATRIX_HEIGHT.equals(reader.getName())) { 510 ret.matrixHeight = Integer.parseInt(reader.getElementText()); 511 } 512 if (QN_MATRIX_WIDTH.equals(reader.getName())) { 513 ret.matrixWidth = Integer.parseInt(reader.getElementText()); 514 } 515 } 516 } 517 if (ret.tileHeight != ret.tileWidth) { 518 throw new AssertionError(tr("Only square tiles are supported. {0}x{1} returned by server for TileMatrix identifier {2}", 519 ret.tileHeight, ret.tileWidth, ret.identifier)); 520 } 521 return ret; 522 } 523 524 /** 525 * Parses OperationMetadata section. Returns when reader is on OperationsMetadata closing tag. 526 * Sets this.baseUrl and this.transferMode 527 * 528 * @param reader StAX reader instance 529 * @throws XMLStreamException See {@link XMLStreamReader} 530 */ 531 private void parseOperationMetadata(XMLStreamReader reader) throws XMLStreamException { 532 for (int event = reader.getEventType(); 533 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && 534 GetCapabilitiesParseHelper.QN_OWS_OPERATIONS_METADATA.equals(reader.getName())); 535 event = reader.next()) { 536 if (event == XMLStreamReader.START_ELEMENT && 537 GetCapabilitiesParseHelper.QN_OWS_OPERATION.equals(reader.getName()) && 538 "GetTile".equals(reader.getAttributeValue("", "name")) && 539 GetCapabilitiesParseHelper.moveReaderToTag(reader, new QName[] { 540 GetCapabilitiesParseHelper.QN_OWS_DCP, 541 GetCapabilitiesParseHelper.QN_OWS_HTTP, 542 GetCapabilitiesParseHelper.QN_OWS_GET, 543 })) { 544 this.baseUrl = reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href"); 545 this.transferMode = GetCapabilitiesParseHelper.getTransferMode(reader); 546 } 547 } 548 } 549 550 /** 551 * Initializes projection for this TileSource with projection 552 * @param proj projection to be used by this TileSource 553 */ 554 public void initProjection(Projection proj) { 555 // getLayers will return only layers matching the name, if the user already choose the layer 556 // so we will not ask the user again to chose the layer, if he just changes projection 557 Collection<Layer> candidates = getLayers(currentLayer != null ? currentLayer.name : defaultLayer, proj.toCode()); 558 if (candidates.size() == 1) { 559 560 Layer newLayer = candidates.iterator().next(); 561 if (newLayer != null) { 562 this.currentTileMatrixSet = newLayer.tileMatrixSet; 563 this.currentLayer = newLayer; 564 Collection<Double> scales = new ArrayList<>(currentTileMatrixSet.tileMatrix.size()); 565 for (TileMatrix tileMatrix : currentTileMatrixSet.tileMatrix) { 566 scales.add(tileMatrix.scaleDenominator * 0.28e-03); 567 } 568 this.nativeScaleList = new ScaleList(scales); 569 } 570 } else if (candidates.size() > 1) { 571 Main.warn("More than one layer WMTS available: {0} for projection {1} and name {2}. Do not know which to process", 572 candidates.stream().map(x -> x.name + ": " + x.tileMatrixSet.identifier).collect(Collectors.joining(", ")), 573 proj.toCode(), 574 currentLayer != null ? currentLayer.name : defaultLayer 575 ); 576 } 577 this.crsScale = getTileSize() * 0.28e-03 / proj.getMetersPerUnit(); 578 } 579 580 /** 581 * 582 * @param name of the layer to match 583 * @param projectionCode projection code to match 584 * @return Collection of layers matching the name of the layer and projection, or only projection if name is not provided 585 */ 586 private Collection<Layer> getLayers(String name, String projectionCode) { 587 Collection<Layer> ret = new ArrayList<>(); 588 if (this.layers != null) { 589 for (Layer layer: this.layers) { 590 if ((name == null || name.equals(layer.name)) && (projectionCode == null || projectionCode.equals(layer.tileMatrixSet.crs))) { 591 ret.add(layer); 592 } 593 } 594 } 595 return ret; 596 } 597 598 @Override 599 public int getTileSize() { 600 // no support for non-square tiles (tileHeight != tileWidth) 601 // and for different tile sizes at different zoom levels 602 Collection<Layer> projLayers = getLayers(null, Main.getProjection().toCode()); 603 if (!projLayers.isEmpty()) { 604 return projLayers.iterator().next().tileMatrixSet.tileMatrix.get(0).tileHeight; 605 } 606 // if no layers is found, fallback to default mercator tile size. Maybe it will work 607 Main.warn("WMTS: Could not determine tile size. Using default tile size of: {0}", getDefaultTileSize()); 608 return getDefaultTileSize(); 609 } 610 611 @Override 612 public String getTileUrl(int zoom, int tilex, int tiley) { 613 if (currentLayer == null) { 614 return ""; 615 } 616 617 String url; 618 if (currentLayer.baseUrl != null && transferMode == null) { 619 url = currentLayer.baseUrl; 620 } else { 621 switch (transferMode) { 622 case KVP: 623 url = baseUrl + URL_GET_ENCODING_PARAMS; 624 break; 625 case REST: 626 url = currentLayer.baseUrl; 627 break; 628 default: 629 url = ""; 630 break; 631 } 632 } 633 634 TileMatrix tileMatrix = getTileMatrix(zoom); 635 636 if (tileMatrix == null) { 637 return ""; // no matrix, probably unsupported CRS selected. 638 } 639 640 return url.replaceAll("\\{layer\\}", this.currentLayer.name) 641 .replaceAll("\\{format\\}", this.currentLayer.format) 642 .replaceAll("\\{TileMatrixSet\\}", this.currentTileMatrixSet.identifier) 643 .replaceAll("\\{TileMatrix\\}", tileMatrix.identifier) 644 .replaceAll("\\{TileRow\\}", Integer.toString(tiley)) 645 .replaceAll("\\{TileCol\\}", Integer.toString(tilex)) 646 .replaceAll("(?i)\\{style\\}", this.currentLayer.style); 647 } 648 649 /** 650 * 651 * @param zoom zoom level 652 * @return TileMatrix that's working on this zoom level 653 */ 654 private TileMatrix getTileMatrix(int zoom) { 655 if (zoom > getMaxZoom()) { 656 return null; 657 } 658 if (zoom < 0) { 659 return null; 660 } 661 return this.currentTileMatrixSet.tileMatrix.get(zoom); 662 } 663 664 @Override 665 public double getDistance(double lat1, double lon1, double lat2, double lon2) { 666 throw new UnsupportedOperationException("Not implemented"); 667 } 668 669 @Override 670 public ICoordinate tileXYToLatLon(Tile tile) { 671 return tileXYToLatLon(tile.getXtile(), tile.getYtile(), tile.getZoom()); 672 } 673 674 @Override 675 public ICoordinate tileXYToLatLon(TileXY xy, int zoom) { 676 return tileXYToLatLon(xy.getXIndex(), xy.getYIndex(), zoom); 677 } 678 679 @Override 680 public ICoordinate tileXYToLatLon(int x, int y, int zoom) { 681 TileMatrix matrix = getTileMatrix(zoom); 682 if (matrix == null) { 683 return Main.getProjection().getWorldBoundsLatLon().getCenter().toCoordinate(); 684 } 685 double scale = matrix.scaleDenominator * this.crsScale; 686 EastNorth ret = new EastNorth(matrix.topLeftCorner.east() + x * scale, matrix.topLeftCorner.north() - y * scale); 687 return Main.getProjection().eastNorth2latlon(ret).toCoordinate(); 688 } 689 690 @Override 691 public TileXY latLonToTileXY(double lat, double lon, int zoom) { 692 TileMatrix matrix = getTileMatrix(zoom); 693 if (matrix == null) { 694 return new TileXY(0, 0); 695 } 696 697 Projection proj = Main.getProjection(); 698 EastNorth enPoint = proj.latlon2eastNorth(new LatLon(lat, lon)); 699 double scale = matrix.scaleDenominator * this.crsScale; 700 return new TileXY( 701 (enPoint.east() - matrix.topLeftCorner.east()) / scale, 702 (matrix.topLeftCorner.north() - enPoint.north()) / scale 703 ); 704 } 705 706 @Override 707 public TileXY latLonToTileXY(ICoordinate point, int zoom) { 708 return latLonToTileXY(point.getLat(), point.getLon(), zoom); 709 } 710 711 @Override 712 public int getTileXMax(int zoom) { 713 return getTileXMax(zoom, Main.getProjection()); 714 } 715 716 @Override 717 public int getTileXMin(int zoom) { 718 return 0; 719 } 720 721 @Override 722 public int getTileYMax(int zoom) { 723 return getTileYMax(zoom, Main.getProjection()); 724 } 725 726 @Override 727 public int getTileYMin(int zoom) { 728 return 0; 729 } 730 731 @Override 732 public Point latLonToXY(double lat, double lon, int zoom) { 733 TileMatrix matrix = getTileMatrix(zoom); 734 if (matrix == null) { 735 return new Point(0, 0); 736 } 737 double scale = matrix.scaleDenominator * this.crsScale; 738 EastNorth point = Main.getProjection().latlon2eastNorth(new LatLon(lat, lon)); 739 return new Point( 740 (int) Math.round((point.east() - matrix.topLeftCorner.east()) / scale), 741 (int) Math.round((matrix.topLeftCorner.north() - point.north()) / scale) 742 ); 743 } 744 745 @Override 746 public Point latLonToXY(ICoordinate point, int zoom) { 747 return latLonToXY(point.getLat(), point.getLon(), zoom); 748 } 749 750 @Override 751 public Coordinate xyToLatLon(Point point, int zoom) { 752 return xyToLatLon(point.x, point.y, zoom); 753 } 754 755 @Override 756 public Coordinate xyToLatLon(int x, int y, int zoom) { 757 TileMatrix matrix = getTileMatrix(zoom); 758 if (matrix == null) { 759 return new Coordinate(0, 0); 760 } 761 double scale = matrix.scaleDenominator * this.crsScale; 762 Projection proj = Main.getProjection(); 763 EastNorth ret = new EastNorth( 764 matrix.topLeftCorner.east() + x * scale, 765 matrix.topLeftCorner.north() - y * scale 766 ); 767 LatLon ll = proj.eastNorth2latlon(ret); 768 return new Coordinate(ll.lat(), ll.lon()); 769 } 770 771 @Override 772 public Map<String, String> getHeaders() { 773 return headers; 774 } 775 776 @Override 777 public int getMaxZoom() { 778 if (this.currentTileMatrixSet != null) { 779 return this.currentTileMatrixSet.tileMatrix.size()-1; 780 } 781 return 0; 782 } 783 784 @Override 785 public String getTileId(int zoom, int tilex, int tiley) { 786 return getTileUrl(zoom, tilex, tiley); 787 } 788 789 /** 790 * Checks if url is acceptable by this Tile Source 791 * @param url URL to check 792 */ 793 public static void checkUrl(String url) { 794 CheckParameterUtil.ensureParameterNotNull(url, "url"); 795 Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url); 796 while (m.find()) { 797 boolean isSupportedPattern = false; 798 for (String pattern : ALL_PATTERNS) { 799 if (m.group().matches(pattern)) { 800 isSupportedPattern = true; 801 break; 802 } 803 } 804 if (!isSupportedPattern) { 805 throw new IllegalArgumentException( 806 tr("{0} is not a valid WMS argument. Please check this server URL:\n{1}", m.group(), url)); 807 } 808 } 809 } 810 811 /** 812 * @return set of projection codes that this TileSource supports 813 */ 814 public Set<String> getSupportedProjections() { 815 Set<String> ret = new HashSet<>(); 816 if (currentLayer == null) { 817 for (Layer layer: this.layers) { 818 ret.add(layer.tileMatrixSet.crs); 819 } 820 } else { 821 for (Layer layer: this.layers) { 822 if (currentLayer.name.equals(layer.name)) { 823 ret.add(layer.tileMatrixSet.crs); 824 } 825 } 826 } 827 return ret; 828 } 829 830 private int getTileYMax(int zoom, Projection proj) { 831 TileMatrix matrix = getTileMatrix(zoom); 832 if (matrix == null) { 833 return 0; 834 } 835 836 if (matrix.matrixHeight != -1) { 837 return matrix.matrixHeight; 838 } 839 840 double scale = matrix.scaleDenominator * this.crsScale; 841 EastNorth min = matrix.topLeftCorner; 842 EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax()); 843 return (int) Math.ceil(Math.abs(max.north() - min.north()) / scale); 844 } 845 846 private int getTileXMax(int zoom, Projection proj) { 847 TileMatrix matrix = getTileMatrix(zoom); 848 if (matrix == null) { 849 return 0; 850 } 851 if (matrix.matrixWidth != -1) { 852 return matrix.matrixWidth; 853 } 854 855 double scale = matrix.scaleDenominator * this.crsScale; 856 EastNorth min = matrix.topLeftCorner; 857 EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax()); 858 return (int) Math.ceil(Math.abs(max.east() - min.east()) / scale); 859 } 860 861 /** 862 * Get native scales of tile source. 863 * @return {@link ScaleList} of native scales 864 */ 865 public ScaleList getNativeScales() { 866 return nativeScaleList; 867 } 868 869}