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