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