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).getValue() 177 .stream() 178 .map(x -> x.name) 179 .collect(Collectors.joining(", ")); //this should be only one 180 case 1: 181 return SelectLayerDialog.this.layers.get(rowIndex).getValue() 182 .stream() 183 .map(x -> x.tileMatrixSet.crs) 184 .collect(Collectors.joining(", ")); 185 case 2: 186 return SelectLayerDialog.this.layers.get(rowIndex).getValue() 187 .stream() 188 .map(x -> x.tileMatrixSet.identifier) 189 .collect(Collectors.joining(", ")); //this should be only one 190 default: 191 throw new IllegalArgumentException(); 192 } 193 } 194 195 @Override 196 public int getRowCount() { 197 return SelectLayerDialog.this.layers.size(); 198 } 199 200 @Override 201 public int getColumnCount() { 202 return 3; 203 } 204 205 @Override 206 public String getColumnName(int column) { 207 switch (column) { 208 case 0: return tr("Layer name"); 209 case 1: return tr("Projection"); 210 case 2: return tr("Matrix set identifier"); 211 default: 212 throw new IllegalArgumentException(); 213 } 214 } 215 216 @Override 217 public boolean isCellEditable(int row, int column) { 218 return false; 219 } 220 }); 221 this.list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 222 this.list.setRowSelectionAllowed(true); 223 this.list.setColumnSelectionAllowed(false); 224 JPanel panel = new JPanel(new GridBagLayout()); 225 panel.add(new JScrollPane(this.list), GBC.eol().fill()); 226 setContent(panel); 227 } 228 229 public DefaultLayer getSelectedLayer() { 230 int index = list.getSelectedRow(); 231 if (index < 0) { 232 return null; //nothing selected 233 } 234 Layer selectedLayer = layers.get(index).getValue().iterator().next(); 235 return new WMTSDefaultLayer(selectedLayer.name, selectedLayer.tileMatrixSet.identifier); 236 } 237 } 238 239 private final Map<String, String> headers = new ConcurrentHashMap<>(); 240 private final Collection<Layer> layers; 241 private Layer currentLayer; 242 private TileMatrixSet currentTileMatrixSet; 243 private double crsScale; 244 private GetCapabilitiesParseHelper.TransferMode transferMode; 245 246 private ScaleList nativeScaleList; 247 248 private final WMTSDefaultLayer defaultLayer; 249 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 : (WMTSDefaultLayer) 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 DefaultLayer userSelectLayer() { 273 Collection<Entry<String, List<Layer>>> grouppedLayers = groupLayersByName(layers); 274 275 // if there is only one layer name no point in asking 276 if (grouppedLayers.size() == 1) { 277 Layer selectedLayer = grouppedLayers.iterator().next().getValue().iterator().next(); 278 return new WMTSDefaultLayer(selectedLayer.name, selectedLayer.tileMatrixSet.identifier); 279 } 280 281 final SelectLayerDialog layerSelection = new SelectLayerDialog(layers); 282 if (layerSelection.showDialog().getValue() == 1) { 283 return layerSelection.getSelectedLayer(); 284 } 285 return null; 286 } 287 288 private String handleTemplate(String url) { 289 Pattern pattern = Pattern.compile(PATTERN_HEADER); 290 StringBuffer output = new StringBuffer(); 291 Matcher matcher = pattern.matcher(url); 292 while (matcher.find()) { 293 this.headers.put(matcher.group(1), matcher.group(2)); 294 matcher.appendReplacement(output, ""); 295 } 296 matcher.appendTail(output); 297 return output.toString(); 298 } 299 300 private static List<Entry<String, List<Layer>>> groupLayersByName(Collection<Layer> layers) { 301 Map<String, List<Layer>> layerByName = layers.stream().collect( 302 Collectors.groupingBy(x -> x.name + '\u001c' + x.tileMatrixSet.identifier)); 303 return layerByName.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList()); 304 } 305 306 /** 307 * @return capabilities 308 * @throws IOException in case of any I/O error 309 * @throws IllegalArgumentException in case of any other error 310 */ 311 private Collection<Layer> getCapabilities() throws IOException { 312 try (CachedFile cf = new CachedFile(baseUrl); InputStream in = cf.setHttpHeaders(headers). 313 setMaxAge(7 * CachedFile.DAYS). 314 setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince). 315 getInputStream()) { 316 byte[] data = Utils.readBytesFromStream(in); 317 if (data.length == 0) { 318 cf.clear(); 319 throw new IllegalArgumentException("Could not read data from: " + baseUrl); 320 } 321 322 try { 323 XMLStreamReader reader = GetCapabilitiesParseHelper.getReader(new ByteArrayInputStream(data)); 324 Collection<Layer> ret = new ArrayList<>(); 325 for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) { 326 if (event == XMLStreamReader.START_ELEMENT) { 327 if (GetCapabilitiesParseHelper.QN_OWS_OPERATIONS_METADATA.equals(reader.getName())) { 328 parseOperationMetadata(reader); 329 } 330 331 if (QN_CONTENTS.equals(reader.getName())) { 332 ret = parseContents(reader); 333 } 334 } 335 } 336 return ret; 337 } catch (XMLStreamException e) { 338 cf.clear(); 339 Main.warn(new String(data, StandardCharsets.UTF_8)); 340 throw new IllegalArgumentException(e); 341 } 342 } 343 } 344 345 /** 346 * Parse Contents tag. Returns when reader reaches Contents closing tag 347 * 348 * @param reader StAX reader instance 349 * @return collection of layers within contents with properly linked TileMatrixSets 350 * @throws XMLStreamException See {@link XMLStreamReader} 351 */ 352 private static Collection<Layer> parseContents(XMLStreamReader reader) throws XMLStreamException { 353 Map<String, TileMatrixSet> matrixSetById = new ConcurrentHashMap<>(); 354 Collection<Layer> layers = new ArrayList<>(); 355 for (int event = reader.getEventType(); 356 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_CONTENTS.equals(reader.getName())); 357 event = reader.next()) { 358 if (event == XMLStreamReader.START_ELEMENT) { 359 if (QN_LAYER.equals(reader.getName())) { 360 layers.add(parseLayer(reader)); 361 } 362 if (QN_TILEMATRIXSET.equals(reader.getName())) { 363 TileMatrixSet entry = parseTileMatrixSet(reader); 364 matrixSetById.put(entry.identifier, entry); 365 } 366 } 367 } 368 Collection<Layer> ret = new ArrayList<>(); 369 // link layers to matrix sets 370 for (Layer l: layers) { 371 for (String tileMatrixId: l.tileMatrixSetLinks) { 372 Layer newLayer = new Layer(l); // create a new layer object for each tile matrix set supported 373 newLayer.tileMatrixSet = matrixSetById.get(tileMatrixId); 374 ret.add(newLayer); 375 } 376 } 377 return ret; 378 } 379 380 /** 381 * Parse Layer tag. Returns when reader will reach Layer closing tag 382 * 383 * @param reader StAX reader instance 384 * @return Layer object, with tileMatrixSetLinks and no tileMatrixSet attribute set. 385 * @throws XMLStreamException See {@link XMLStreamReader} 386 */ 387 private static Layer parseLayer(XMLStreamReader reader) throws XMLStreamException { 388 Layer layer = new Layer(); 389 Stack<QName> tagStack = new Stack<>(); 390 391 for (int event = reader.getEventType(); 392 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_LAYER.equals(reader.getName())); 393 event = reader.next()) { 394 if (event == XMLStreamReader.START_ELEMENT) { 395 tagStack.push(reader.getName()); 396 if (tagStack.size() == 2) { 397 if (QN_FORMAT.equals(reader.getName())) { 398 layer.format = reader.getElementText(); 399 } else if (GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER.equals(reader.getName())) { 400 layer.name = reader.getElementText(); 401 } else if (QN_RESOURCE_URL.equals(reader.getName()) && 402 "tile".equals(reader.getAttributeValue("", "resourceType"))) { 403 layer.baseUrl = reader.getAttributeValue("", "template"); 404 } else if (QN_STYLE.equals(reader.getName()) && 405 "true".equals(reader.getAttributeValue("", "isDefault"))) { 406 if (GetCapabilitiesParseHelper.moveReaderToTag(reader, new QName[] {GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER})) { 407 layer.style = reader.getElementText(); 408 tagStack.push(reader.getName()); // keep tagStack in sync 409 } 410 } else if (QN_TILEMATRIX_SET_LINK.equals(reader.getName())) { 411 layer.tileMatrixSetLinks.add(praseTileMatrixSetLink(reader)); 412 } else { 413 GetCapabilitiesParseHelper.moveReaderToEndCurrentTag(reader); 414 } 415 } 416 } 417 // need to get event type from reader, as parsing might have change position of reader 418 if (reader.getEventType() == XMLStreamReader.END_ELEMENT) { 419 QName start = tagStack.pop(); 420 if (!start.equals(reader.getName())) { 421 throw new IllegalStateException(tr("WMTS Parser error - start element {0} has different name than end element {2}", 422 start, reader.getName())); 423 } 424 } 425 } 426 if (layer.style == null) { 427 layer.style = ""; 428 } 429 return layer; 430 } 431 432 /** 433 * Gets TileMatrixSetLink value. Returns when reader is on TileMatrixSetLink closing tag 434 * 435 * @param reader StAX reader instance 436 * @return TileMatrixSetLink identifier 437 * @throws XMLStreamException See {@link XMLStreamReader} 438 */ 439 private static String praseTileMatrixSetLink(XMLStreamReader reader) throws XMLStreamException { 440 String ret = null; 441 for (int event = reader.getEventType(); 442 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && 443 QN_TILEMATRIX_SET_LINK.equals(reader.getName())); 444 event = reader.next()) { 445 if (event == XMLStreamReader.START_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName())) { 446 ret = reader.getElementText(); 447 } 448 } 449 return ret; 450 } 451 452 /** 453 * Parses TileMatrixSet section. Returns when reader is on TileMatrixSet closing tag 454 * @param reader StAX reader instance 455 * @return TileMatrixSet object 456 * @throws XMLStreamException See {@link XMLStreamReader} 457 */ 458 private static TileMatrixSet parseTileMatrixSet(XMLStreamReader reader) throws XMLStreamException { 459 TileMatrixSetBuilder matrixSet = new TileMatrixSetBuilder(); 460 for (int event = reader.getEventType(); 461 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName())); 462 event = reader.next()) { 463 if (event == XMLStreamReader.START_ELEMENT) { 464 if (GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER.equals(reader.getName())) { 465 matrixSet.identifier = reader.getElementText(); 466 } 467 if (GetCapabilitiesParseHelper.QN_OWS_SUPPORTED_CRS.equals(reader.getName())) { 468 matrixSet.crs = GetCapabilitiesParseHelper.crsToCode(reader.getElementText()); 469 } 470 if (QN_TILEMATRIX.equals(reader.getName())) { 471 matrixSet.tileMatrix.add(parseTileMatrix(reader, matrixSet.crs)); 472 } 473 } 474 } 475 return matrixSet.build(); 476 } 477 478 /** 479 * Parses TileMatrix section. Returns when reader is on TileMatrix closing tag. 480 * @param reader StAX reader instance 481 * @param matrixCrs projection used by this matrix 482 * @return TileMatrix object 483 * @throws XMLStreamException See {@link XMLStreamReader} 484 */ 485 private static TileMatrix parseTileMatrix(XMLStreamReader reader, String matrixCrs) throws XMLStreamException { 486 Projection matrixProj = Projections.getProjectionByCode(matrixCrs); 487 TileMatrix ret = new TileMatrix(); 488 489 if (matrixProj == null) { 490 // use current projection if none found. Maybe user is using custom string 491 matrixProj = Main.getProjection(); 492 } 493 for (int event = reader.getEventType(); 494 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_TILEMATRIX.equals(reader.getName())); 495 event = reader.next()) { 496 if (event == XMLStreamReader.START_ELEMENT) { 497 if (GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER.equals(reader.getName())) { 498 ret.identifier = reader.getElementText(); 499 } 500 if (QN_SCALE_DENOMINATOR.equals(reader.getName())) { 501 ret.scaleDenominator = Double.parseDouble(reader.getElementText()); 502 } 503 if (QN_TOPLEFT_CORNER.equals(reader.getName())) { 504 String[] topLeftCorner = reader.getElementText().split(" "); 505 if (matrixProj.switchXY()) { 506 ret.topLeftCorner = new EastNorth(Double.parseDouble(topLeftCorner[1]), Double.parseDouble(topLeftCorner[0])); 507 } else { 508 ret.topLeftCorner = new EastNorth(Double.parseDouble(topLeftCorner[0]), Double.parseDouble(topLeftCorner[1])); 509 } 510 } 511 if (QN_TILE_HEIGHT.equals(reader.getName())) { 512 ret.tileHeight = Integer.parseInt(reader.getElementText()); 513 } 514 if (QN_TILE_WIDTH.equals(reader.getName())) { 515 ret.tileWidth = Integer.parseInt(reader.getElementText()); 516 } 517 if (QN_MATRIX_HEIGHT.equals(reader.getName())) { 518 ret.matrixHeight = Integer.parseInt(reader.getElementText()); 519 } 520 if (QN_MATRIX_WIDTH.equals(reader.getName())) { 521 ret.matrixWidth = Integer.parseInt(reader.getElementText()); 522 } 523 } 524 } 525 if (ret.tileHeight != ret.tileWidth) { 526 throw new AssertionError(tr("Only square tiles are supported. {0}x{1} returned by server for TileMatrix identifier {2}", 527 ret.tileHeight, ret.tileWidth, ret.identifier)); 528 } 529 return ret; 530 } 531 532 /** 533 * Parses OperationMetadata section. Returns when reader is on OperationsMetadata closing tag. 534 * Sets this.baseUrl and this.transferMode 535 * 536 * @param reader StAX reader instance 537 * @throws XMLStreamException See {@link XMLStreamReader} 538 */ 539 private void parseOperationMetadata(XMLStreamReader reader) throws XMLStreamException { 540 for (int event = reader.getEventType(); 541 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && 542 GetCapabilitiesParseHelper.QN_OWS_OPERATIONS_METADATA.equals(reader.getName())); 543 event = reader.next()) { 544 if (event == XMLStreamReader.START_ELEMENT && 545 GetCapabilitiesParseHelper.QN_OWS_OPERATION.equals(reader.getName()) && 546 "GetTile".equals(reader.getAttributeValue("", "name")) && 547 GetCapabilitiesParseHelper.moveReaderToTag(reader, new QName[] { 548 GetCapabilitiesParseHelper.QN_OWS_DCP, 549 GetCapabilitiesParseHelper.QN_OWS_HTTP, 550 GetCapabilitiesParseHelper.QN_OWS_GET, 551 })) { 552 this.baseUrl = reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href"); 553 this.transferMode = GetCapabilitiesParseHelper.getTransferMode(reader); 554 } 555 } 556 } 557 558 /** 559 * Initializes projection for this TileSource with projection 560 * @param proj projection to be used by this TileSource 561 */ 562 public void initProjection(Projection proj) { 563 // getLayers will return only layers matching the name, if the user already choose the layer 564 // so we will not ask the user again to chose the layer, if he just changes projection 565 Collection<Layer> candidates = getLayers( 566 currentLayer != null ? new WMTSDefaultLayer(currentLayer.name, currentLayer.tileMatrixSet.identifier) : defaultLayer, 567 proj.toCode()); 568 569 if (candidates.size() == 1) { 570 Layer newLayer = candidates.iterator().next(); 571 if (newLayer != null) { 572 this.currentTileMatrixSet = newLayer.tileMatrixSet; 573 this.currentLayer = newLayer; 574 Collection<Double> scales = new ArrayList<>(currentTileMatrixSet.tileMatrix.size()); 575 for (TileMatrix tileMatrix : currentTileMatrixSet.tileMatrix) { 576 scales.add(tileMatrix.scaleDenominator * 0.28e-03); 577 } 578 this.nativeScaleList = new ScaleList(scales); 579 } 580 } else if (candidates.size() > 1) { 581 Main.warn("More than one layer WMTS available: {0} for projection {1} and name {2}. Do not know which to process", 582 candidates.stream().map(x -> x.name + ": " + x.tileMatrixSet.identifier).collect(Collectors.joining(", ")), 583 proj.toCode(), 584 currentLayer != null ? currentLayer.name : defaultLayer 585 ); 586 } 587 this.crsScale = getTileSize() * 0.28e-03 / proj.getMetersPerUnit(); 588 } 589 590 /** 591 * 592 * @param searchLayer which layer do we look for 593 * @param projectionCode projection code to match 594 * @return Collection of layers matching the name of the layer and projection, or only projection if name is not provided 595 */ 596 private Collection<Layer> getLayers(WMTSDefaultLayer searchLayer, String projectionCode) { 597 Collection<Layer> ret = new ArrayList<>(); 598 if (this.layers != null) { 599 for (Layer layer: this.layers) { 600 if ((searchLayer == null || (// if it's null, then accept all layers 601 searchLayer.getLayerName().equals(layer.name) && 602 searchLayer.getTileMatrixSet().equals(layer.tileMatrixSet.identifier))) 603 && (projectionCode == null || // if it's null, then accept any projection 604 projectionCode.equals(layer.tileMatrixSet.crs))) { 605 ret.add(layer); 606 } 607 } 608 } 609 return ret; 610 } 611 612 @Override 613 public int getTileSize() { 614 // no support for non-square tiles (tileHeight != tileWidth) 615 // and for different tile sizes at different zoom levels 616 Collection<Layer> projLayers = getLayers(null, Main.getProjection().toCode()); 617 if (!projLayers.isEmpty()) { 618 return projLayers.iterator().next().tileMatrixSet.tileMatrix.get(0).tileHeight; 619 } 620 // if no layers is found, fallback to default mercator tile size. Maybe it will work 621 Main.warn("WMTS: Could not determine tile size. Using default tile size of: {0}", getDefaultTileSize()); 622 return getDefaultTileSize(); 623 } 624 625 @Override 626 public String getTileUrl(int zoom, int tilex, int tiley) { 627 if (currentLayer == null) { 628 return ""; 629 } 630 631 String url; 632 if (currentLayer.baseUrl != null && transferMode == null) { 633 url = currentLayer.baseUrl; 634 } else { 635 switch (transferMode) { 636 case KVP: 637 url = baseUrl + URL_GET_ENCODING_PARAMS; 638 break; 639 case REST: 640 url = currentLayer.baseUrl; 641 break; 642 default: 643 url = ""; 644 break; 645 } 646 } 647 648 TileMatrix tileMatrix = getTileMatrix(zoom); 649 650 if (tileMatrix == null) { 651 return ""; // no matrix, probably unsupported CRS selected. 652 } 653 654 return url.replaceAll("\\{layer\\}", this.currentLayer.name) 655 .replaceAll("\\{format\\}", this.currentLayer.format) 656 .replaceAll("\\{TileMatrixSet\\}", this.currentTileMatrixSet.identifier) 657 .replaceAll("\\{TileMatrix\\}", tileMatrix.identifier) 658 .replaceAll("\\{TileRow\\}", Integer.toString(tiley)) 659 .replaceAll("\\{TileCol\\}", Integer.toString(tilex)) 660 .replaceAll("(?i)\\{style\\}", this.currentLayer.style); 661 } 662 663 /** 664 * 665 * @param zoom zoom level 666 * @return TileMatrix that's working on this zoom level 667 */ 668 private TileMatrix getTileMatrix(int zoom) { 669 if (zoom > getMaxZoom()) { 670 return null; 671 } 672 if (zoom < 0) { 673 return null; 674 } 675 return this.currentTileMatrixSet.tileMatrix.get(zoom); 676 } 677 678 @Override 679 public double getDistance(double lat1, double lon1, double lat2, double lon2) { 680 throw new UnsupportedOperationException("Not implemented"); 681 } 682 683 @Override 684 public ICoordinate tileXYToLatLon(Tile tile) { 685 return tileXYToLatLon(tile.getXtile(), tile.getYtile(), tile.getZoom()); 686 } 687 688 @Override 689 public ICoordinate tileXYToLatLon(TileXY xy, int zoom) { 690 return tileXYToLatLon(xy.getXIndex(), xy.getYIndex(), zoom); 691 } 692 693 @Override 694 public ICoordinate tileXYToLatLon(int x, int y, int zoom) { 695 TileMatrix matrix = getTileMatrix(zoom); 696 if (matrix == null) { 697 return Main.getProjection().getWorldBoundsLatLon().getCenter().toCoordinate(); 698 } 699 double scale = matrix.scaleDenominator * this.crsScale; 700 EastNorth ret = new EastNorth(matrix.topLeftCorner.east() + x * scale, matrix.topLeftCorner.north() - y * scale); 701 return Main.getProjection().eastNorth2latlon(ret).toCoordinate(); 702 } 703 704 @Override 705 public TileXY latLonToTileXY(double lat, double lon, int zoom) { 706 TileMatrix matrix = getTileMatrix(zoom); 707 if (matrix == null) { 708 return new TileXY(0, 0); 709 } 710 711 Projection proj = Main.getProjection(); 712 EastNorth enPoint = proj.latlon2eastNorth(new LatLon(lat, lon)); 713 double scale = matrix.scaleDenominator * this.crsScale; 714 return new TileXY( 715 (enPoint.east() - matrix.topLeftCorner.east()) / scale, 716 (matrix.topLeftCorner.north() - enPoint.north()) / scale 717 ); 718 } 719 720 @Override 721 public TileXY latLonToTileXY(ICoordinate point, int zoom) { 722 return latLonToTileXY(point.getLat(), point.getLon(), zoom); 723 } 724 725 @Override 726 public int getTileXMax(int zoom) { 727 return getTileXMax(zoom, Main.getProjection()); 728 } 729 730 @Override 731 public int getTileXMin(int zoom) { 732 return 0; 733 } 734 735 @Override 736 public int getTileYMax(int zoom) { 737 return getTileYMax(zoom, Main.getProjection()); 738 } 739 740 @Override 741 public int getTileYMin(int zoom) { 742 return 0; 743 } 744 745 @Override 746 public Point latLonToXY(double lat, double lon, int zoom) { 747 TileMatrix matrix = getTileMatrix(zoom); 748 if (matrix == null) { 749 return new Point(0, 0); 750 } 751 double scale = matrix.scaleDenominator * this.crsScale; 752 EastNorth point = Main.getProjection().latlon2eastNorth(new LatLon(lat, lon)); 753 return new Point( 754 (int) Math.round((point.east() - matrix.topLeftCorner.east()) / scale), 755 (int) Math.round((matrix.topLeftCorner.north() - point.north()) / scale) 756 ); 757 } 758 759 @Override 760 public Point latLonToXY(ICoordinate point, int zoom) { 761 return latLonToXY(point.getLat(), point.getLon(), zoom); 762 } 763 764 @Override 765 public Coordinate xyToLatLon(Point point, int zoom) { 766 return xyToLatLon(point.x, point.y, zoom); 767 } 768 769 @Override 770 public Coordinate xyToLatLon(int x, int y, int zoom) { 771 TileMatrix matrix = getTileMatrix(zoom); 772 if (matrix == null) { 773 return new Coordinate(0, 0); 774 } 775 double scale = matrix.scaleDenominator * this.crsScale; 776 Projection proj = Main.getProjection(); 777 EastNorth ret = new EastNorth( 778 matrix.topLeftCorner.east() + x * scale, 779 matrix.topLeftCorner.north() - y * scale 780 ); 781 LatLon ll = proj.eastNorth2latlon(ret); 782 return new Coordinate(ll.lat(), ll.lon()); 783 } 784 785 @Override 786 public Map<String, String> getHeaders() { 787 return headers; 788 } 789 790 @Override 791 public int getMaxZoom() { 792 if (this.currentTileMatrixSet != null) { 793 return this.currentTileMatrixSet.tileMatrix.size()-1; 794 } 795 return 0; 796 } 797 798 @Override 799 public String getTileId(int zoom, int tilex, int tiley) { 800 return getTileUrl(zoom, tilex, tiley); 801 } 802 803 /** 804 * Checks if url is acceptable by this Tile Source 805 * @param url URL to check 806 */ 807 public static void checkUrl(String url) { 808 CheckParameterUtil.ensureParameterNotNull(url, "url"); 809 Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url); 810 while (m.find()) { 811 boolean isSupportedPattern = false; 812 for (String pattern : ALL_PATTERNS) { 813 if (m.group().matches(pattern)) { 814 isSupportedPattern = true; 815 break; 816 } 817 } 818 if (!isSupportedPattern) { 819 throw new IllegalArgumentException( 820 tr("{0} is not a valid WMS argument. Please check this server URL:\n{1}", m.group(), url)); 821 } 822 } 823 } 824 825 /** 826 * @return set of projection codes that this TileSource supports 827 */ 828 public Set<String> getSupportedProjections() { 829 Set<String> ret = new HashSet<>(); 830 if (currentLayer == null) { 831 for (Layer layer: this.layers) { 832 ret.add(layer.tileMatrixSet.crs); 833 } 834 } else { 835 for (Layer layer: this.layers) { 836 if (currentLayer.name.equals(layer.name)) { 837 ret.add(layer.tileMatrixSet.crs); 838 } 839 } 840 } 841 return ret; 842 } 843 844 private int getTileYMax(int zoom, Projection proj) { 845 TileMatrix matrix = getTileMatrix(zoom); 846 if (matrix == null) { 847 return 0; 848 } 849 850 if (matrix.matrixHeight != -1) { 851 return matrix.matrixHeight; 852 } 853 854 double scale = matrix.scaleDenominator * this.crsScale; 855 EastNorth min = matrix.topLeftCorner; 856 EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax()); 857 return (int) Math.ceil(Math.abs(max.north() - min.north()) / scale); 858 } 859 860 private int getTileXMax(int zoom, Projection proj) { 861 TileMatrix matrix = getTileMatrix(zoom); 862 if (matrix == null) { 863 return 0; 864 } 865 if (matrix.matrixWidth != -1) { 866 return matrix.matrixWidth; 867 } 868 869 double scale = matrix.scaleDenominator * this.crsScale; 870 EastNorth min = matrix.topLeftCorner; 871 EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax()); 872 return (int) Math.ceil(Math.abs(max.east() - min.east()) / scale); 873 } 874 875 /** 876 * Get native scales of tile source. 877 * @return {@link ScaleList} of native scales 878 */ 879 public ScaleList getNativeScales() { 880 return nativeScaleList; 881 } 882 883}