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 * @throws IllegalArgumentException if any other error happens for the given imagery info 255 */ 256 public WMTSTileSource(ImageryInfo info) throws IOException { 257 super(info); 258 this.baseUrl = normalizeCapabilitiesUrl(handleTemplate(info.getUrl())); 259 this.layers = getCapabilities(); 260 if (this.layers.isEmpty()) 261 throw new IllegalArgumentException(tr("No layers defined by getCapabilities document: {0}", info.getUrl())); 262 } 263 264 private static Layer userSelectLayer(Collection<Layer> layers) { 265 if (layers.size() == 1) 266 return layers.iterator().next(); 267 Layer ret = null; 268 269 final SelectLayerDialog layerSelection = new SelectLayerDialog(layers); 270 if (layerSelection.showDialog().getValue() == 1) { 271 ret = layerSelection.getSelectedLayer(); 272 // TODO: save layer information into ImageryInfo / ImageryPreferences? 273 } 274 if (ret == null) { 275 // user canceled operation or did not choose any layer 276 throw new IllegalArgumentException(tr("No layer selected")); 277 } 278 return ret; 279 } 280 281 private String handleTemplate(String url) { 282 Pattern pattern = Pattern.compile(PATTERN_HEADER); 283 StringBuffer output = new StringBuffer(); // NOSONAR 284 Matcher matcher = pattern.matcher(url); 285 while (matcher.find()) { 286 this.headers.put(matcher.group(1), matcher.group(2)); 287 matcher.appendReplacement(output, ""); 288 } 289 matcher.appendTail(output); 290 return output.toString(); 291 } 292 293 /** 294 * @return capabilities 295 * @throws IOException in case of any I/O error 296 * @throws IllegalArgumentException in case of any other error 297 */ 298 private Collection<Layer> getCapabilities() throws IOException { 299 XMLInputFactory factory = XMLInputFactory.newFactory(); 300 // do not try to load external entities, nor validate the XML 301 factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE); 302 factory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE); 303 factory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE); 304 305 try (CachedFile cf = new CachedFile(baseUrl); InputStream in = cf.setHttpHeaders(headers). 306 setMaxAge(7 * CachedFile.DAYS). 307 setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince). 308 getInputStream()) { 309 byte[] data = Utils.readBytesFromStream(in); 310 if (data == null || data.length == 0) { 311 throw new IllegalArgumentException("Could not read data from: " + baseUrl); 312 } 313 XMLStreamReader reader = factory.createXMLStreamReader(new ByteArrayInputStream(data)); 314 315 Collection<Layer> ret = new ArrayList<>(); 316 for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) { 317 if (event == XMLStreamReader.START_ELEMENT) { 318 if (new QName(OWS_NS_URL, "OperationsMetadata").equals(reader.getName())) { 319 parseOperationMetadata(reader); 320 } 321 322 if (new QName(WMTS_NS_URL, "Contents").equals(reader.getName())) { 323 ret = parseContents(reader); 324 } 325 } 326 } 327 return ret; 328 } catch (XMLStreamException e) { 329 throw new IllegalArgumentException(e); 330 } 331 } 332 333 /** 334 * Parse Contents tag. Renturns when reader reaches Contents closing tag 335 * 336 * @param reader StAX reader instance 337 * @return collection of layers within contents with properly linked TileMatrixSets 338 * @throws XMLStreamException See {@link XMLStreamReader} 339 */ 340 private static Collection<Layer> parseContents(XMLStreamReader reader) throws XMLStreamException { 341 Map<String, TileMatrixSet> matrixSetById = new ConcurrentHashMap<>(); 342 Collection<Layer> layers = new ArrayList<>(); 343 for (int event = reader.getEventType(); 344 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && new QName(WMTS_NS_URL, "Contents").equals(reader.getName())); 345 event = reader.next()) { 346 if (event == XMLStreamReader.START_ELEMENT) { 347 if (new QName(WMTS_NS_URL, "Layer").equals(reader.getName())) { 348 layers.add(parseLayer(reader)); 349 } 350 if (new QName(WMTS_NS_URL, "TileMatrixSet").equals(reader.getName())) { 351 TileMatrixSet entry = parseTileMatrixSet(reader); 352 matrixSetById.put(entry.identifier, entry); 353 } 354 } 355 } 356 Collection<Layer> ret = new ArrayList<>(); 357 // link layers to matrix sets 358 for (Layer l: layers) { 359 for (String tileMatrixId: l.tileMatrixSetLinks) { 360 Layer newLayer = new Layer(l); // create a new layer object for each tile matrix set supported 361 newLayer.tileMatrixSet = matrixSetById.get(tileMatrixId); 362 ret.add(newLayer); 363 } 364 } 365 return ret; 366 } 367 368 /** 369 * Parse Layer tag. Returns when reader will reach Layer closing tag 370 * 371 * @param reader StAX reader instance 372 * @return Layer object, with tileMatrixSetLinks and no tileMatrixSet attribute set. 373 * @throws XMLStreamException See {@link XMLStreamReader} 374 */ 375 private static Layer parseLayer(XMLStreamReader reader) throws XMLStreamException { 376 Layer layer = new Layer(); 377 Stack<QName> tagStack = new Stack<>(); 378 379 for (int event = reader.getEventType(); 380 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && new QName(WMTS_NS_URL, "Layer").equals(reader.getName())); 381 event = reader.next()) { 382 if (event == XMLStreamReader.START_ELEMENT) { 383 tagStack.push(reader.getName()); 384 if (tagStack.size() == 2) { 385 if (new QName(WMTS_NS_URL, "Format").equals(reader.getName())) { 386 layer.format = reader.getElementText(); 387 } else if (new QName(OWS_NS_URL, "Identifier").equals(reader.getName())) { 388 layer.name = reader.getElementText(); 389 } else if (new QName(WMTS_NS_URL, "ResourceURL").equals(reader.getName()) && 390 "tile".equals(reader.getAttributeValue("", "resourceType"))) { 391 layer.baseUrl = reader.getAttributeValue("", "template"); 392 } else if (new QName(WMTS_NS_URL, "Style").equals(reader.getName()) && 393 "true".equals(reader.getAttributeValue("", "isDefault"))) { 394 if (moveReaderToTag(reader, new QName[] {new QName(OWS_NS_URL, "Identifier")})) { 395 layer.style = reader.getElementText(); 396 tagStack.push(reader.getName()); // keep tagStack in sync 397 } 398 } else if (new QName(WMTS_NS_URL, "TileMatrixSetLink").equals(reader.getName())) { 399 layer.tileMatrixSetLinks.add(praseTileMatrixSetLink(reader)); 400 } else { 401 moveReaderToEndCurrentTag(reader); 402 } 403 } 404 } 405 // need to get event type from reader, as parsing might have change position of reader 406 if (reader.getEventType() == XMLStreamReader.END_ELEMENT) { 407 QName start = tagStack.pop(); 408 if (!start.equals(reader.getName())) { 409 throw new IllegalStateException(tr("WMTS Parser error - start element {0} has different name than end element {2}", 410 start, reader.getName())); 411 } 412 } 413 } 414 if (layer.style == null) { 415 layer.style = ""; 416 } 417 return layer; 418 } 419 420 /** 421 * Moves the reader to the closing tag of current tag. 422 * @param reader XML stream reader positioned on XMLStreamReader.START_ELEMENT 423 * @throws XMLStreamException when parse exception occurs 424 */ 425 private static void moveReaderToEndCurrentTag(XMLStreamReader reader) throws XMLStreamException { 426 int level = 0; 427 QName tag = reader.getName(); 428 for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) { 429 switch (event) { 430 case XMLStreamReader.START_ELEMENT: 431 level += 1; 432 break; 433 case XMLStreamReader.END_ELEMENT: 434 level -= 1; 435 if (level == 0 && tag.equals(reader.getName())) { 436 return; 437 } 438 } 439 if (level < 0) { 440 throw new IllegalStateException("WMTS Parser error - moveReaderToEndCurrentTag failed to find closing tag"); 441 } 442 } 443 throw new IllegalStateException("WMTS Parser error - moveReaderToEndCurrentTag failed to find closing tag"); 444 445 } 446 447 /** 448 * Gets TileMatrixSetLink value. Returns when reader is on TileMatrixSetLink closing tag 449 * 450 * @param reader StAX reader instance 451 * @return TileMatrixSetLink identifier 452 * @throws XMLStreamException See {@link XMLStreamReader} 453 */ 454 private static String praseTileMatrixSetLink(XMLStreamReader reader) throws XMLStreamException { 455 String ret = null; 456 for (int event = reader.getEventType(); 457 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && 458 new QName(WMTS_NS_URL, "TileMatrixSetLink").equals(reader.getName())); 459 event = reader.next()) { 460 if (event == XMLStreamReader.START_ELEMENT && new QName(WMTS_NS_URL, "TileMatrixSet").equals(reader.getName())) { 461 ret = reader.getElementText(); 462 } 463 } 464 return ret; 465 } 466 467 /** 468 * Parses TileMatrixSet section. Returns when reader is on TileMatrixSet closing tag 469 * @param reader StAX reader instance 470 * @return TileMatrixSet object 471 * @throws XMLStreamException See {@link XMLStreamReader} 472 */ 473 private static TileMatrixSet parseTileMatrixSet(XMLStreamReader reader) throws XMLStreamException { 474 TileMatrixSetBuilder matrixSet = new TileMatrixSetBuilder(); 475 for (int event = reader.getEventType(); 476 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && new QName(WMTS_NS_URL, "TileMatrixSet").equals(reader.getName())); 477 event = reader.next()) { 478 if (event == XMLStreamReader.START_ELEMENT) { 479 if (new QName(OWS_NS_URL, "Identifier").equals(reader.getName())) { 480 matrixSet.identifier = reader.getElementText(); 481 } 482 if (new QName(OWS_NS_URL, "SupportedCRS").equals(reader.getName())) { 483 matrixSet.crs = crsToCode(reader.getElementText()); 484 } 485 if (new QName(WMTS_NS_URL, "TileMatrix").equals(reader.getName())) { 486 matrixSet.tileMatrix.add(parseTileMatrix(reader, matrixSet.crs)); 487 } 488 } 489 } 490 return matrixSet.build(); 491 } 492 493 /** 494 * Parses TileMatrix section. Returns when reader is on TileMatrix closing tag. 495 * @param reader StAX reader instance 496 * @param matrixCrs projection used by this matrix 497 * @return TileMatrix object 498 * @throws XMLStreamException See {@link XMLStreamReader} 499 */ 500 private static TileMatrix parseTileMatrix(XMLStreamReader reader, String matrixCrs) throws XMLStreamException { 501 Projection matrixProj = Projections.getProjectionByCode(matrixCrs); 502 TileMatrix ret = new TileMatrix(); 503 504 if (matrixProj == null) { 505 // use current projection if none found. Maybe user is using custom string 506 matrixProj = Main.getProjection(); 507 } 508 for (int event = reader.getEventType(); 509 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && new QName(WMTS_NS_URL, "TileMatrix").equals(reader.getName())); 510 event = reader.next()) { 511 if (event == XMLStreamReader.START_ELEMENT) { 512 if (new QName(OWS_NS_URL, "Identifier").equals(reader.getName())) { 513 ret.identifier = reader.getElementText(); 514 } 515 if (new QName(WMTS_NS_URL, "ScaleDenominator").equals(reader.getName())) { 516 ret.scaleDenominator = Double.parseDouble(reader.getElementText()); 517 } 518 if (new QName(WMTS_NS_URL, "TopLeftCorner").equals(reader.getName())) { 519 String[] topLeftCorner = reader.getElementText().split(" "); 520 if (matrixProj.switchXY()) { 521 ret.topLeftCorner = new EastNorth(Double.parseDouble(topLeftCorner[1]), Double.parseDouble(topLeftCorner[0])); 522 } else { 523 ret.topLeftCorner = new EastNorth(Double.parseDouble(topLeftCorner[0]), Double.parseDouble(topLeftCorner[1])); 524 } 525 } 526 if (new QName(WMTS_NS_URL, "TileHeight").equals(reader.getName())) { 527 ret.tileHeight = Integer.parseInt(reader.getElementText()); 528 } 529 if (new QName(WMTS_NS_URL, "TileWidth").equals(reader.getName())) { 530 ret.tileWidth = Integer.parseInt(reader.getElementText()); 531 } 532 if (new QName(WMTS_NS_URL, "MatrixHeight").equals(reader.getName())) { 533 ret.matrixHeight = Integer.parseInt(reader.getElementText()); 534 } 535 if (new QName(WMTS_NS_URL, "MatrixWidth").equals(reader.getName())) { 536 ret.matrixWidth = Integer.parseInt(reader.getElementText()); 537 } 538 } 539 } 540 if (ret.tileHeight != ret.tileWidth) { 541 throw new AssertionError(tr("Only square tiles are supported. {0}x{1} returned by server for TileMatrix identifier {2}", 542 ret.tileHeight, ret.tileWidth, ret.identifier)); 543 } 544 return ret; 545 } 546 547 /** 548 * Parses OperationMetadata section. Returns when reader is on OperationsMetadata closing tag. 549 * Sets this.baseUrl and this.transferMode 550 * 551 * @param reader StAX reader instance 552 * @throws XMLStreamException See {@link XMLStreamReader} 553 */ 554 private void parseOperationMetadata(XMLStreamReader reader) throws XMLStreamException { 555 for (int event = reader.getEventType(); 556 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && 557 new QName(OWS_NS_URL, "OperationsMetadata").equals(reader.getName())); 558 event = reader.next()) { 559 if (event == XMLStreamReader.START_ELEMENT) { 560 if (new QName(OWS_NS_URL, "Operation").equals(reader.getName()) && "GetTile".equals(reader.getAttributeValue("", "name")) && 561 moveReaderToTag(reader, new QName[]{ 562 new QName(OWS_NS_URL, "DCP"), 563 new QName(OWS_NS_URL, "HTTP"), 564 new QName(OWS_NS_URL, "Get"), 565 566 })) { 567 this.baseUrl = reader.getAttributeValue(XLINK_NS_URL, "href"); 568 this.transferMode = getTransferMode(reader); 569 } 570 } 571 } 572 } 573 574 /** 575 * Parses Operation[@name='GetTile']/DCP/HTTP/Get section. Returns when reader is on Get closing tag. 576 * @param reader StAX reader instance 577 * @return TransferMode coded in this section 578 * @throws XMLStreamException See {@link XMLStreamReader} 579 */ 580 private static TransferMode getTransferMode(XMLStreamReader reader) throws XMLStreamException { 581 QName getQname = new QName(OWS_NS_URL, "Get"); 582 583 Utils.ensure(getQname.equals(reader.getName()), "WMTS Parser state invalid. Expected element %s, got %s", 584 getQname, reader.getName()); 585 for (int event = reader.getEventType(); 586 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && getQname.equals(reader.getName())); 587 event = reader.next()) { 588 if (event == XMLStreamReader.START_ELEMENT && new QName(OWS_NS_URL, "Constraint").equals(reader.getName()) 589 && "GetEncoding".equals(reader.getAttributeValue("", "name"))) { 590 moveReaderToTag(reader, new QName[]{ 591 new QName(OWS_NS_URL, "AllowedValues"), 592 new QName(OWS_NS_URL, "Value") 593 }); 594 return TransferMode.fromString(reader.getElementText()); 595 } 596 } 597 return null; 598 } 599 600 /** 601 * Moves reader to first occurrence of the structure equivalent of Xpath tags[0]/tags[1]../tags[n]. If fails to find 602 * moves the reader to the closing tag of current tag 603 * 604 * @param reader StAX reader instance 605 * @param tags array of tags 606 * @return true if tag was found, false otherwise 607 * @throws XMLStreamException See {@link XMLStreamReader} 608 */ 609 private static boolean moveReaderToTag(XMLStreamReader reader, QName[] tags) throws XMLStreamException { 610 QName stopTag = reader.getName(); 611 int currentLevel = 0; 612 QName searchTag = tags[currentLevel]; 613 QName parentTag = null; 614 QName skipTag = null; 615 616 for (int event = 0; //skip current element, so we will not skip it as a whole 617 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && stopTag.equals(reader.getName())); 618 event = reader.next()) { 619 if (event == XMLStreamReader.END_ELEMENT && skipTag != null && skipTag.equals(reader.getName())) { 620 skipTag = null; 621 } 622 if (skipTag == null) { 623 if (event == XMLStreamReader.START_ELEMENT) { 624 if (searchTag.equals(reader.getName())) { 625 currentLevel += 1; 626 if (currentLevel >= tags.length) { 627 return true; // found! 628 } 629 parentTag = searchTag; 630 searchTag = tags[currentLevel]; 631 } else { 632 skipTag = reader.getName(); 633 } 634 } 635 636 if (event == XMLStreamReader.END_ELEMENT && parentTag != null && parentTag.equals(reader.getName())) { 637 currentLevel -= 1; 638 searchTag = parentTag; 639 if (currentLevel >= 0) { 640 parentTag = tags[currentLevel]; 641 } else { 642 parentTag = null; 643 } 644 } 645 } 646 } 647 return false; 648 } 649 650 private static String normalizeCapabilitiesUrl(String url) throws MalformedURLException { 651 URL inUrl = new URL(url); 652 URL ret = new URL(inUrl.getProtocol(), inUrl.getHost(), inUrl.getPort(), inUrl.getFile()); 653 return ret.toExternalForm(); 654 } 655 656 private static String crsToCode(String crsIdentifier) { 657 if (crsIdentifier.startsWith("urn:ogc:def:crs:")) { 658 return crsIdentifier.replaceFirst("urn:ogc:def:crs:([^:]*):.*:(.*)$", "$1:$2"); 659 } 660 return crsIdentifier; 661 } 662 663 /** 664 * Initializes projection for this TileSource with projection 665 * @param proj projection to be used by this TileSource 666 */ 667 public void initProjection(Projection proj) { 668 // getLayers will return only layers matching the name, if the user already choose the layer 669 // so we will not ask the user again to chose the layer, if he just changes projection 670 Collection<Layer> candidates = getLayers(currentLayer != null ? currentLayer.name : null, proj.toCode()); 671 if (!candidates.isEmpty()) { 672 Layer newLayer = userSelectLayer(candidates); 673 if (newLayer != null) { 674 this.currentTileMatrixSet = newLayer.tileMatrixSet; 675 this.currentLayer = newLayer; 676 Collection<Double> scales = new ArrayList<>(currentTileMatrixSet.tileMatrix.size()); 677 for (TileMatrix tileMatrix : currentTileMatrixSet.tileMatrix) { 678 scales.add(tileMatrix.scaleDenominator * 0.28e-03); 679 } 680 this.nativeScaleList = new ScaleList(scales); 681 } 682 } 683 this.crsScale = getTileSize() * 0.28e-03 / proj.getMetersPerUnit(); 684 } 685 686 /** 687 * 688 * @param name of the layer to match 689 * @param projectionCode projection code to match 690 * @return Collection of layers matching the name of the layer and projection, or only projection if name is not provided 691 */ 692 private Collection<Layer> getLayers(String name, String projectionCode) { 693 Collection<Layer> ret = new ArrayList<>(); 694 if (this.layers != null) { 695 for (Layer layer: this.layers) { 696 if ((name == null || name.equals(layer.name)) && (projectionCode == null || projectionCode.equals(layer.tileMatrixSet.crs))) { 697 ret.add(layer); 698 } 699 } 700 } 701 return ret; 702 } 703 704 @Override 705 public int getTileSize() { 706 // no support for non-square tiles (tileHeight != tileWidth) 707 // and for different tile sizes at different zoom levels 708 Collection<Layer> layers = getLayers(null, Main.getProjection().toCode()); 709 if (!layers.isEmpty()) { 710 return layers.iterator().next().tileMatrixSet.tileMatrix.get(0).tileHeight; 711 } 712 // if no layers is found, fallback to default mercator tile size. Maybe it will work 713 Main.warn("WMTS: Could not determine tile size. Using default tile size of: {0}", getDefaultTileSize()); 714 return getDefaultTileSize(); 715 } 716 717 @Override 718 public String getTileUrl(int zoom, int tilex, int tiley) { 719 String url; 720 if (currentLayer == null) { 721 return ""; 722 } 723 724 if (currentLayer.baseUrl != null && transferMode == null) { 725 url = currentLayer.baseUrl; 726 } else { 727 switch (transferMode) { 728 case KVP: 729 url = baseUrl + URL_GET_ENCODING_PARAMS; 730 break; 731 case REST: 732 url = currentLayer.baseUrl; 733 break; 734 default: 735 url = ""; 736 break; 737 } 738 } 739 740 TileMatrix tileMatrix = getTileMatrix(zoom); 741 742 if (tileMatrix == null) { 743 return ""; // no matrix, probably unsupported CRS selected. 744 } 745 746 return url.replaceAll("\\{layer\\}", this.currentLayer.name) 747 .replaceAll("\\{format\\}", this.currentLayer.format) 748 .replaceAll("\\{TileMatrixSet\\}", this.currentTileMatrixSet.identifier) 749 .replaceAll("\\{TileMatrix\\}", tileMatrix.identifier) 750 .replaceAll("\\{TileRow\\}", Integer.toString(tiley)) 751 .replaceAll("\\{TileCol\\}", Integer.toString(tilex)) 752 .replaceAll("(?i)\\{style\\}", this.currentLayer.style); 753 } 754 755 /** 756 * 757 * @param zoom zoom level 758 * @return TileMatrix that's working on this zoom level 759 */ 760 private TileMatrix getTileMatrix(int zoom) { 761 if (zoom > getMaxZoom()) { 762 return null; 763 } 764 if (zoom < 0) { 765 return null; 766 } 767 return this.currentTileMatrixSet.tileMatrix.get(zoom); 768 } 769 770 @Override 771 public double getDistance(double lat1, double lon1, double lat2, double lon2) { 772 throw new UnsupportedOperationException("Not implemented"); 773 } 774 775 @Override 776 public ICoordinate tileXYToLatLon(Tile tile) { 777 return tileXYToLatLon(tile.getXtile(), tile.getYtile(), tile.getZoom()); 778 } 779 780 @Override 781 public ICoordinate tileXYToLatLon(TileXY xy, int zoom) { 782 return tileXYToLatLon(xy.getXIndex(), xy.getYIndex(), zoom); 783 } 784 785 @Override 786 public ICoordinate tileXYToLatLon(int x, int y, int zoom) { 787 TileMatrix matrix = getTileMatrix(zoom); 788 if (matrix == null) { 789 return Main.getProjection().getWorldBoundsLatLon().getCenter().toCoordinate(); 790 } 791 double scale = matrix.scaleDenominator * this.crsScale; 792 EastNorth ret = new EastNorth(matrix.topLeftCorner.east() + x * scale, matrix.topLeftCorner.north() - y * scale); 793 return Main.getProjection().eastNorth2latlon(ret).toCoordinate(); 794 } 795 796 @Override 797 public TileXY latLonToTileXY(double lat, double lon, int zoom) { 798 TileMatrix matrix = getTileMatrix(zoom); 799 if (matrix == null) { 800 return new TileXY(0, 0); 801 } 802 803 Projection proj = Main.getProjection(); 804 EastNorth enPoint = proj.latlon2eastNorth(new LatLon(lat, lon)); 805 double scale = matrix.scaleDenominator * this.crsScale; 806 return new TileXY( 807 (enPoint.east() - matrix.topLeftCorner.east()) / scale, 808 (matrix.topLeftCorner.north() - enPoint.north()) / scale 809 ); 810 } 811 812 @Override 813 public TileXY latLonToTileXY(ICoordinate point, int zoom) { 814 return latLonToTileXY(point.getLat(), point.getLon(), zoom); 815 } 816 817 @Override 818 public int getTileXMax(int zoom) { 819 return getTileXMax(zoom, Main.getProjection()); 820 } 821 822 @Override 823 public int getTileXMin(int zoom) { 824 return 0; 825 } 826 827 @Override 828 public int getTileYMax(int zoom) { 829 return getTileYMax(zoom, Main.getProjection()); 830 } 831 832 @Override 833 public int getTileYMin(int zoom) { 834 return 0; 835 } 836 837 @Override 838 public Point latLonToXY(double lat, double lon, int zoom) { 839 TileMatrix matrix = getTileMatrix(zoom); 840 if (matrix == null) { 841 return new Point(0, 0); 842 } 843 double scale = matrix.scaleDenominator * this.crsScale; 844 EastNorth point = Main.getProjection().latlon2eastNorth(new LatLon(lat, lon)); 845 return new Point( 846 (int) Math.round((point.east() - matrix.topLeftCorner.east()) / scale), 847 (int) Math.round((matrix.topLeftCorner.north() - point.north()) / scale) 848 ); 849 } 850 851 @Override 852 public Point latLonToXY(ICoordinate point, int zoom) { 853 return latLonToXY(point.getLat(), point.getLon(), zoom); 854 } 855 856 @Override 857 public Coordinate xyToLatLon(Point point, int zoom) { 858 return xyToLatLon(point.x, point.y, zoom); 859 } 860 861 @Override 862 public Coordinate xyToLatLon(int x, int y, int zoom) { 863 TileMatrix matrix = getTileMatrix(zoom); 864 if (matrix == null) { 865 return new Coordinate(0, 0); 866 } 867 double scale = matrix.scaleDenominator * this.crsScale; 868 Projection proj = Main.getProjection(); 869 EastNorth ret = new EastNorth( 870 matrix.topLeftCorner.east() + x * scale, 871 matrix.topLeftCorner.north() - y * scale 872 ); 873 LatLon ll = proj.eastNorth2latlon(ret); 874 return new Coordinate(ll.lat(), ll.lon()); 875 } 876 877 @Override 878 public Map<String, String> getHeaders() { 879 return headers; 880 } 881 882 @Override 883 public int getMaxZoom() { 884 if (this.currentTileMatrixSet != null) { 885 return this.currentTileMatrixSet.tileMatrix.size()-1; 886 } 887 return 0; 888 } 889 890 @Override 891 public String getTileId(int zoom, int tilex, int tiley) { 892 return getTileUrl(zoom, tilex, tiley); 893 } 894 895 /** 896 * Checks if url is acceptable by this Tile Source 897 * @param url URL to check 898 */ 899 public static void checkUrl(String url) { 900 CheckParameterUtil.ensureParameterNotNull(url, "url"); 901 Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url); 902 while (m.find()) { 903 boolean isSupportedPattern = false; 904 for (String pattern : ALL_PATTERNS) { 905 if (m.group().matches(pattern)) { 906 isSupportedPattern = true; 907 break; 908 } 909 } 910 if (!isSupportedPattern) { 911 throw new IllegalArgumentException( 912 tr("{0} is not a valid WMS argument. Please check this server URL:\n{1}", m.group(), url)); 913 } 914 } 915 } 916 917 /** 918 * @return set of projection codes that this TileSource supports 919 */ 920 public Set<String> getSupportedProjections() { 921 Set<String> ret = new HashSet<>(); 922 if (currentLayer == null) { 923 for (Layer layer: this.layers) { 924 ret.add(layer.tileMatrixSet.crs); 925 } 926 } else { 927 for (Layer layer: this.layers) { 928 if (currentLayer.name.equals(layer.name)) { 929 ret.add(layer.tileMatrixSet.crs); 930 } 931 } 932 } 933 return ret; 934 } 935 936 private int getTileYMax(int zoom, Projection proj) { 937 TileMatrix matrix = getTileMatrix(zoom); 938 if (matrix == null) { 939 return 0; 940 } 941 942 if (matrix.matrixHeight != -1) { 943 return matrix.matrixHeight; 944 } 945 946 double scale = matrix.scaleDenominator * this.crsScale; 947 EastNorth min = matrix.topLeftCorner; 948 EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax()); 949 return (int) Math.ceil(Math.abs(max.north() - min.north()) / scale); 950 } 951 952 private int getTileXMax(int zoom, Projection proj) { 953 TileMatrix matrix = getTileMatrix(zoom); 954 if (matrix == null) { 955 return 0; 956 } 957 if (matrix.matrixWidth != -1) { 958 return matrix.matrixWidth; 959 } 960 961 double scale = matrix.scaleDenominator * this.crsScale; 962 EastNorth min = matrix.topLeftCorner; 963 EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax()); 964 return (int) Math.ceil(Math.abs(max.east() - min.east()) / scale); 965 } 966 967 /** 968 * Get native scales of tile source. 969 * @return {@link ScaleList} of native scales 970 */ 971 public ScaleList getNativeScales() { 972 return nativeScaleList; 973 } 974 975}