001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.imagery; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.GridBagLayout; 007import java.awt.Point; 008import java.io.ByteArrayInputStream; 009import java.io.IOException; 010import java.io.InputStream; 011import java.nio.charset.StandardCharsets; 012import java.util.ArrayList; 013import java.util.Arrays; 014import java.util.Collection; 015import java.util.Collections; 016import java.util.LinkedHashSet; 017import java.util.List; 018import java.util.Map; 019import java.util.Map.Entry; 020import java.util.Objects; 021import java.util.Optional; 022import java.util.SortedSet; 023import java.util.Stack; 024import java.util.TreeSet; 025import java.util.concurrent.ConcurrentHashMap; 026import java.util.regex.Matcher; 027import java.util.regex.Pattern; 028import java.util.stream.Collectors; 029 030import javax.imageio.ImageIO; 031import javax.swing.JPanel; 032import javax.swing.JScrollPane; 033import javax.swing.JTable; 034import javax.swing.ListSelectionModel; 035import javax.swing.table.AbstractTableModel; 036import javax.xml.namespace.QName; 037import javax.xml.stream.XMLStreamException; 038import javax.xml.stream.XMLStreamReader; 039 040import org.openstreetmap.gui.jmapviewer.Coordinate; 041import org.openstreetmap.gui.jmapviewer.Projected; 042import org.openstreetmap.gui.jmapviewer.Tile; 043import org.openstreetmap.gui.jmapviewer.TileRange; 044import org.openstreetmap.gui.jmapviewer.TileXY; 045import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; 046import org.openstreetmap.gui.jmapviewer.interfaces.IProjected; 047import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource; 048import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource; 049import org.openstreetmap.josm.Main; 050import org.openstreetmap.josm.data.ProjectionBounds; 051import org.openstreetmap.josm.data.coor.EastNorth; 052import org.openstreetmap.josm.data.coor.LatLon; 053import org.openstreetmap.josm.data.projection.Projection; 054import org.openstreetmap.josm.data.projection.Projections; 055import org.openstreetmap.josm.gui.ExtendedDialog; 056import org.openstreetmap.josm.gui.layer.NativeScaleLayer.ScaleList; 057import org.openstreetmap.josm.io.CachedFile; 058import org.openstreetmap.josm.spi.preferences.Config; 059import org.openstreetmap.josm.tools.CheckParameterUtil; 060import org.openstreetmap.josm.tools.GBC; 061import org.openstreetmap.josm.tools.Logging; 062import org.openstreetmap.josm.tools.Utils; 063 064/** 065 * Tile Source handling WMTS providers 066 * 067 * @author Wiktor Niesiobędzki 068 * @since 8526 069 */ 070public class WMTSTileSource extends AbstractTMSTileSource implements TemplatedTileSource { 071 /** 072 * WMTS namespace address 073 */ 074 public static final String WMTS_NS_URL = "http://www.opengis.net/wmts/1.0"; 075 076 // CHECKSTYLE.OFF: SingleSpaceSeparator 077 private static final QName QN_CONTENTS = new QName(WMTSTileSource.WMTS_NS_URL, "Contents"); 078 private static final QName QN_DEFAULT = new QName(WMTSTileSource.WMTS_NS_URL, "Default"); 079 private static final QName QN_DIMENSION = new QName(WMTSTileSource.WMTS_NS_URL, "Dimension"); 080 private static final QName QN_FORMAT = new QName(WMTSTileSource.WMTS_NS_URL, "Format"); 081 private static final QName QN_LAYER = new QName(WMTSTileSource.WMTS_NS_URL, "Layer"); 082 private static final QName QN_MATRIX_WIDTH = new QName(WMTSTileSource.WMTS_NS_URL, "MatrixWidth"); 083 private static final QName QN_MATRIX_HEIGHT = new QName(WMTSTileSource.WMTS_NS_URL, "MatrixHeight"); 084 private static final QName QN_RESOURCE_URL = new QName(WMTSTileSource.WMTS_NS_URL, "ResourceURL"); 085 private static final QName QN_SCALE_DENOMINATOR = new QName(WMTSTileSource.WMTS_NS_URL, "ScaleDenominator"); 086 private static final QName QN_STYLE = new QName(WMTSTileSource.WMTS_NS_URL, "Style"); 087 private static final QName QN_TILEMATRIX = new QName(WMTSTileSource.WMTS_NS_URL, "TileMatrix"); 088 private static final QName QN_TILEMATRIXSET = new QName(WMTSTileSource.WMTS_NS_URL, "TileMatrixSet"); 089 private static final QName QN_TILEMATRIX_SET_LINK = new QName(WMTSTileSource.WMTS_NS_URL, "TileMatrixSetLink"); 090 private static final QName QN_TILE_WIDTH = new QName(WMTSTileSource.WMTS_NS_URL, "TileWidth"); 091 private static final QName QN_TILE_HEIGHT = new QName(WMTSTileSource.WMTS_NS_URL, "TileHeight"); 092 private static final QName QN_TOPLEFT_CORNER = new QName(WMTSTileSource.WMTS_NS_URL, "TopLeftCorner"); 093 private static final QName QN_VALUE = new QName(WMTSTileSource.WMTS_NS_URL, "Value"); 094 // CHECKSTYLE.ON: SingleSpaceSeparator 095 096 private static final String PATTERN_HEADER = "\\{header\\(([^,]+),([^}]+)\\)\\}"; 097 098 private static final String URL_GET_ENCODING_PARAMS = "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER={layer}&STYLE={style}&" 099 + "FORMAT={format}&tileMatrixSet={TileMatrixSet}&tileMatrix={TileMatrix}&tileRow={TileRow}&tileCol={TileCol}"; 100 101 private static final String[] ALL_PATTERNS = { 102 PATTERN_HEADER, 103 }; 104 105 private int cachedTileSize = -1; 106 107 private static class TileMatrix { 108 private String identifier; 109 private double scaleDenominator; 110 private EastNorth topLeftCorner; 111 private int tileWidth; 112 private int tileHeight; 113 private int matrixWidth = -1; 114 private int matrixHeight = -1; 115 } 116 117 private static class TileMatrixSetBuilder { 118 // sorted by zoom level 119 SortedSet<TileMatrix> tileMatrix = new TreeSet<>((o1, o2) -> -1 * Double.compare(o1.scaleDenominator, o2.scaleDenominator)); 120 private String crs; 121 private String identifier; 122 123 TileMatrixSet build() { 124 return new TileMatrixSet(this); 125 } 126 } 127 128 private static class TileMatrixSet { 129 130 private final List<TileMatrix> tileMatrix; 131 private final String crs; 132 private final String identifier; 133 134 TileMatrixSet(TileMatrixSet tileMatrixSet) { 135 if (tileMatrixSet != null) { 136 tileMatrix = new ArrayList<>(tileMatrixSet.tileMatrix); 137 crs = tileMatrixSet.crs; 138 identifier = tileMatrixSet.identifier; 139 } else { 140 tileMatrix = Collections.emptyList(); 141 crs = null; 142 identifier = null; 143 } 144 } 145 146 TileMatrixSet(TileMatrixSetBuilder builder) { 147 tileMatrix = new ArrayList<>(builder.tileMatrix); 148 crs = builder.crs; 149 identifier = builder.identifier; 150 } 151 152 @Override 153 public String toString() { 154 return "TileMatrixSet [crs=" + crs + ", identifier=" + identifier + ']'; 155 } 156 } 157 158 private static class Dimension { 159 private String identifier; 160 private String defaultValue; 161 private final List<String> values = new ArrayList<>(); 162 } 163 164 private static class Layer { 165 private String format; 166 private String identifier; 167 private String title; 168 private TileMatrixSet tileMatrixSet; 169 private String baseUrl; 170 private String style; 171 private final Collection<String> tileMatrixSetLinks = new ArrayList<>(); 172 private final Collection<Dimension> dimensions = new ArrayList<>(); 173 174 Layer(Layer l) { 175 Objects.requireNonNull(l); 176 format = l.format; 177 identifier = l.identifier; 178 title = l.title; 179 baseUrl = l.baseUrl; 180 style = l.style; 181 tileMatrixSet = new TileMatrixSet(l.tileMatrixSet); 182 dimensions.addAll(l.dimensions); 183 } 184 185 Layer() { 186 } 187 188 /** 189 * Get title of the layer for user display. 190 * 191 * This is either the content of the Title element (if available) or 192 * the layer identifier (as fallback) 193 * @return title of the layer for user display 194 */ 195 public String getUserTitle() { 196 return title != null ? title : identifier; 197 } 198 199 @Override 200 public String toString() { 201 return "Layer [identifier=" + identifier + ", title=" + title + ", tileMatrixSet=" 202 + tileMatrixSet + ", baseUrl=" + baseUrl + ", style=" + style + ']'; 203 } 204 } 205 206 private static final class SelectLayerDialog extends ExtendedDialog { 207 private final transient List<Entry<String, List<Layer>>> layers; 208 private final JTable list; 209 210 SelectLayerDialog(Collection<Layer> layers) { 211 super(Main.parent, tr("Select WMTS layer"), tr("Add layers"), tr("Cancel")); 212 this.layers = groupLayersByNameAndTileMatrixSet(layers); 213 //getLayersTable(layers, Main.getProjection()) 214 this.list = new JTable( 215 new AbstractTableModel() { 216 @Override 217 public Object getValueAt(int rowIndex, int columnIndex) { 218 switch (columnIndex) { 219 case 0: 220 return SelectLayerDialog.this.layers.get(rowIndex).getValue() 221 .stream() 222 .map(Layer::getUserTitle) 223 .collect(Collectors.joining(", ")); //this should be only one 224 case 1: 225 return SelectLayerDialog.this.layers.get(rowIndex).getValue() 226 .stream() 227 .map(x -> x.tileMatrixSet.crs) 228 .collect(Collectors.joining(", ")); 229 case 2: 230 return SelectLayerDialog.this.layers.get(rowIndex).getValue() 231 .stream() 232 .map(x -> x.tileMatrixSet.identifier) 233 .collect(Collectors.joining(", ")); //this should be only one 234 default: 235 throw new IllegalArgumentException(); 236 } 237 } 238 239 @Override 240 public int getRowCount() { 241 return SelectLayerDialog.this.layers.size(); 242 } 243 244 @Override 245 public int getColumnCount() { 246 return 3; 247 } 248 249 @Override 250 public String getColumnName(int column) { 251 switch (column) { 252 case 0: return tr("Layer name"); 253 case 1: return tr("Projection"); 254 case 2: return tr("Matrix set identifier"); 255 default: 256 throw new IllegalArgumentException(); 257 } 258 } 259 }); 260 this.list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 261 this.list.setAutoCreateRowSorter(true); 262 this.list.setRowSelectionAllowed(true); 263 this.list.setColumnSelectionAllowed(false); 264 JPanel panel = new JPanel(new GridBagLayout()); 265 panel.add(new JScrollPane(this.list), GBC.eol().fill()); 266 setContent(panel); 267 } 268 269 public DefaultLayer getSelectedLayer() { 270 int index = list.getSelectedRow(); 271 if (index < 0) { 272 return null; //nothing selected 273 } 274 Layer selectedLayer = layers.get(list.convertRowIndexToModel(index)).getValue().get(0); 275 return new WMTSDefaultLayer(selectedLayer.identifier, selectedLayer.tileMatrixSet.identifier); 276 } 277 278 private static List<Entry<String, List<Layer>>> groupLayersByNameAndTileMatrixSet(Collection<Layer> layers) { 279 Map<String, List<Layer>> layerByName = layers.stream().collect( 280 Collectors.groupingBy(x -> x.identifier + '\u001c' + x.tileMatrixSet.identifier)); 281 return layerByName.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList()); 282 } 283 } 284 285 private final Map<String, String> headers = new ConcurrentHashMap<>(); 286 private final Collection<Layer> layers; 287 private Layer currentLayer; 288 private TileMatrixSet currentTileMatrixSet; 289 private double crsScale; 290 private GetCapabilitiesParseHelper.TransferMode transferMode; 291 292 private ScaleList nativeScaleList; 293 294 private final WMTSDefaultLayer defaultLayer; 295 296 private Projection tileProjection; 297 298 /** 299 * Creates a tile source based on imagery info 300 * @param info imagery info 301 * @throws IOException if any I/O error occurs 302 * @throws IllegalArgumentException if any other error happens for the given imagery info 303 */ 304 public WMTSTileSource(ImageryInfo info) throws IOException { 305 super(info); 306 CheckParameterUtil.ensureThat(info.getDefaultLayers().size() < 2, "At most 1 default layer for WMTS is supported"); 307 308 this.baseUrl = GetCapabilitiesParseHelper.normalizeCapabilitiesUrl(handleTemplate(info.getUrl())); 309 this.layers = getCapabilities(); 310 if (info.getDefaultLayers().isEmpty()) { 311 Logging.warn(tr("No default layer selected, choosing first layer.")); 312 if (!layers.isEmpty()) { 313 Layer first = layers.iterator().next(); 314 this.defaultLayer = new WMTSDefaultLayer(first.identifier, first.tileMatrixSet.identifier); 315 } else { 316 this.defaultLayer = null; 317 } 318 } else { 319 DefaultLayer defLayer = info.getDefaultLayers().iterator().next(); 320 if (defLayer instanceof WMTSDefaultLayer) { 321 this.defaultLayer = (WMTSDefaultLayer) defLayer; 322 } else { 323 this.defaultLayer = null; 324 } 325 } 326 if (this.layers.isEmpty()) 327 throw new IllegalArgumentException(tr("No layers defined by getCapabilities document: {0}", info.getUrl())); 328 } 329 330 /** 331 * Creates a dialog based on this tile source with all available layers and returns the name of selected layer 332 * @return Name of selected layer 333 */ 334 public DefaultLayer userSelectLayer() { 335 Map<String, List<Layer>> layerById = layers.stream().collect( 336 Collectors.groupingBy(x -> x.identifier)); 337 if (layerById.size() == 1) { // only one layer 338 List<Layer> ls = layerById.entrySet().iterator().next().getValue() 339 .stream().filter( 340 u -> u.tileMatrixSet.crs.equals(Main.getProjection().toCode())) 341 .collect(Collectors.toList()); 342 if (ls.size() == 1) { 343 // only one tile matrix set with matching projection - no point in asking 344 Layer selectedLayer = ls.get(0); 345 return new WMTSDefaultLayer(selectedLayer.identifier, selectedLayer.tileMatrixSet.identifier); 346 } 347 } 348 349 final SelectLayerDialog layerSelection = new SelectLayerDialog(layers); 350 if (layerSelection.showDialog().getValue() == 1) { 351 return layerSelection.getSelectedLayer(); 352 } 353 return null; 354 } 355 356 private String handleTemplate(String url) { 357 Pattern pattern = Pattern.compile(PATTERN_HEADER); 358 StringBuffer output = new StringBuffer(); 359 Matcher matcher = pattern.matcher(url); 360 while (matcher.find()) { 361 this.headers.put(matcher.group(1), matcher.group(2)); 362 matcher.appendReplacement(output, ""); 363 } 364 matcher.appendTail(output); 365 return output.toString(); 366 } 367 368 /** 369 * @return capabilities 370 * @throws IOException in case of any I/O error 371 * @throws IllegalArgumentException in case of any other error 372 */ 373 private Collection<Layer> getCapabilities() throws IOException { 374 try (CachedFile cf = new CachedFile(baseUrl); InputStream in = cf.setHttpHeaders(headers). 375 setMaxAge(Config.getPref().getLong("wmts.capabilities.cache.max_age", 7 * CachedFile.DAYS)). 376 setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince). 377 getInputStream()) { 378 byte[] data = Utils.readBytesFromStream(in); 379 if (data.length == 0) { 380 cf.clear(); 381 throw new IllegalArgumentException("Could not read data from: " + baseUrl); 382 } 383 384 try { 385 XMLStreamReader reader = GetCapabilitiesParseHelper.getReader(new ByteArrayInputStream(data)); 386 Collection<Layer> ret = new ArrayList<>(); 387 for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) { 388 if (event == XMLStreamReader.START_ELEMENT) { 389 if (GetCapabilitiesParseHelper.QN_OWS_OPERATIONS_METADATA.equals(reader.getName())) { 390 parseOperationMetadata(reader); 391 } 392 393 if (QN_CONTENTS.equals(reader.getName())) { 394 ret = parseContents(reader); 395 } 396 } 397 } 398 return ret; 399 } catch (XMLStreamException e) { 400 cf.clear(); 401 Logging.warn(new String(data, StandardCharsets.UTF_8)); 402 throw new IllegalArgumentException(e); 403 } 404 } 405 } 406 407 /** 408 * Parse Contents tag. Returns when reader reaches Contents closing tag 409 * 410 * @param reader StAX reader instance 411 * @return collection of layers within contents with properly linked TileMatrixSets 412 * @throws XMLStreamException See {@link XMLStreamReader} 413 */ 414 private static Collection<Layer> parseContents(XMLStreamReader reader) throws XMLStreamException { 415 Map<String, TileMatrixSet> matrixSetById = new ConcurrentHashMap<>(); 416 Collection<Layer> layers = new ArrayList<>(); 417 for (int event = reader.getEventType(); 418 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_CONTENTS.equals(reader.getName())); 419 event = reader.next()) { 420 if (event == XMLStreamReader.START_ELEMENT) { 421 if (QN_LAYER.equals(reader.getName())) { 422 Layer l = parseLayer(reader); 423 if (l != null) { 424 layers.add(l); 425 } 426 } 427 if (QN_TILEMATRIXSET.equals(reader.getName())) { 428 TileMatrixSet entry = parseTileMatrixSet(reader); 429 matrixSetById.put(entry.identifier, entry); 430 } 431 } 432 } 433 Collection<Layer> ret = new ArrayList<>(); 434 // link layers to matrix sets 435 for (Layer l: layers) { 436 for (String tileMatrixId: l.tileMatrixSetLinks) { 437 Layer newLayer = new Layer(l); // create a new layer object for each tile matrix set supported 438 newLayer.tileMatrixSet = matrixSetById.get(tileMatrixId); 439 ret.add(newLayer); 440 } 441 } 442 return ret; 443 } 444 445 /** 446 * Parse Layer tag. Returns when reader will reach Layer closing tag 447 * 448 * @param reader StAX reader instance 449 * @return Layer object, with tileMatrixSetLinks and no tileMatrixSet attribute set. 450 * @throws XMLStreamException See {@link XMLStreamReader} 451 */ 452 private static Layer parseLayer(XMLStreamReader reader) throws XMLStreamException { 453 Layer layer = new Layer(); 454 Stack<QName> tagStack = new Stack<>(); 455 List<String> supportedMimeTypes = new ArrayList<>(Arrays.asList(ImageIO.getReaderMIMETypes())); 456 supportedMimeTypes.add("image/jpgpng"); // used by ESRI 457 supportedMimeTypes.add("image/png8"); // used by geoserver 458 Collection<String> unsupportedFormats = new ArrayList<>(); 459 460 for (int event = reader.getEventType(); 461 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_LAYER.equals(reader.getName())); 462 event = reader.next()) { 463 if (event == XMLStreamReader.START_ELEMENT) { 464 tagStack.push(reader.getName()); 465 if (tagStack.size() == 2) { 466 if (QN_FORMAT.equals(reader.getName())) { 467 String format = reader.getElementText(); 468 if (supportedMimeTypes.contains(format)) { 469 layer.format = format; 470 } else { 471 unsupportedFormats.add(format); 472 } 473 } else if (GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER.equals(reader.getName())) { 474 layer.identifier = reader.getElementText(); 475 } else if (GetCapabilitiesParseHelper.QN_OWS_TITLE.equals(reader.getName())) { 476 layer.title = reader.getElementText(); 477 } else if (QN_RESOURCE_URL.equals(reader.getName()) && 478 "tile".equals(reader.getAttributeValue("", "resourceType"))) { 479 layer.baseUrl = reader.getAttributeValue("", "template"); 480 } else if (QN_STYLE.equals(reader.getName()) && 481 "true".equals(reader.getAttributeValue("", "isDefault"))) { 482 if (GetCapabilitiesParseHelper.moveReaderToTag(reader, GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER)) { 483 layer.style = reader.getElementText(); 484 tagStack.push(reader.getName()); // keep tagStack in sync 485 } 486 } else if (QN_DIMENSION.equals(reader.getName())) { 487 layer.dimensions.add(parseDimension(reader)); 488 } else if (QN_TILEMATRIX_SET_LINK.equals(reader.getName())) { 489 layer.tileMatrixSetLinks.add(parseTileMatrixSetLink(reader)); 490 } else { 491 GetCapabilitiesParseHelper.moveReaderToEndCurrentTag(reader); 492 } 493 } 494 } 495 // need to get event type from reader, as parsing might have change position of reader 496 if (reader.getEventType() == XMLStreamReader.END_ELEMENT) { 497 QName start = tagStack.pop(); 498 if (!start.equals(reader.getName())) { 499 throw new IllegalStateException(tr("WMTS Parser error - start element {0} has different name than end element {2}", 500 start, reader.getName())); 501 } 502 } 503 } 504 if (layer.style == null) { 505 layer.style = ""; 506 } 507 if (layer.format == null) { 508 // no format found - it's mandatory parameter - can't use this layer 509 Logging.warn(tr("Can''t use layer {0} because no supported formats where found. Layer is available in formats: {1}", 510 layer.getUserTitle(), 511 String.join(", ", unsupportedFormats))); 512 return null; 513 } 514 return layer; 515 } 516 517 /** 518 * Gets Dimension value. Returns when reader is on Dimension closing tag 519 * 520 * @param reader StAX reader instance 521 * @return dimension 522 * @throws XMLStreamException See {@link XMLStreamReader} 523 */ 524 private static Dimension parseDimension(XMLStreamReader reader) throws XMLStreamException { 525 Dimension ret = new Dimension(); 526 for (int event = reader.getEventType(); 527 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && 528 QN_DIMENSION.equals(reader.getName())); 529 event = reader.next()) { 530 if (event == XMLStreamReader.START_ELEMENT) { 531 if (GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER.equals(reader.getName())) { 532 ret.identifier = reader.getElementText(); 533 } else if (QN_DEFAULT.equals(reader.getName())) { 534 ret.defaultValue = reader.getElementText(); 535 } else if (QN_VALUE.equals(reader.getName())) { 536 ret.values.add(reader.getElementText()); 537 } 538 } 539 } 540 return ret; 541 } 542 543 /** 544 * Gets TileMatrixSetLink value. Returns when reader is on TileMatrixSetLink closing tag 545 * 546 * @param reader StAX reader instance 547 * @return TileMatrixSetLink identifier 548 * @throws XMLStreamException See {@link XMLStreamReader} 549 */ 550 private static String parseTileMatrixSetLink(XMLStreamReader reader) throws XMLStreamException { 551 String ret = null; 552 for (int event = reader.getEventType(); 553 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && 554 QN_TILEMATRIX_SET_LINK.equals(reader.getName())); 555 event = reader.next()) { 556 if (event == XMLStreamReader.START_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName())) { 557 ret = reader.getElementText(); 558 } 559 } 560 return ret; 561 } 562 563 /** 564 * Parses TileMatrixSet section. Returns when reader is on TileMatrixSet closing tag 565 * @param reader StAX reader instance 566 * @return TileMatrixSet object 567 * @throws XMLStreamException See {@link XMLStreamReader} 568 */ 569 private static TileMatrixSet parseTileMatrixSet(XMLStreamReader reader) throws XMLStreamException { 570 TileMatrixSetBuilder matrixSet = new TileMatrixSetBuilder(); 571 for (int event = reader.getEventType(); 572 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName())); 573 event = reader.next()) { 574 if (event == XMLStreamReader.START_ELEMENT) { 575 if (GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER.equals(reader.getName())) { 576 matrixSet.identifier = reader.getElementText(); 577 } 578 if (GetCapabilitiesParseHelper.QN_OWS_SUPPORTED_CRS.equals(reader.getName())) { 579 matrixSet.crs = GetCapabilitiesParseHelper.crsToCode(reader.getElementText()); 580 } 581 if (QN_TILEMATRIX.equals(reader.getName())) { 582 matrixSet.tileMatrix.add(parseTileMatrix(reader, matrixSet.crs)); 583 } 584 } 585 } 586 return matrixSet.build(); 587 } 588 589 /** 590 * Parses TileMatrix section. Returns when reader is on TileMatrix closing tag. 591 * @param reader StAX reader instance 592 * @param matrixCrs projection used by this matrix 593 * @return TileMatrix object 594 * @throws XMLStreamException See {@link XMLStreamReader} 595 */ 596 private static TileMatrix parseTileMatrix(XMLStreamReader reader, String matrixCrs) throws XMLStreamException { 597 Projection matrixProj = Optional.ofNullable(Projections.getProjectionByCode(matrixCrs)) 598 .orElseGet(Main::getProjection); // use current projection if none found. Maybe user is using custom string 599 TileMatrix ret = new TileMatrix(); 600 for (int event = reader.getEventType(); 601 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_TILEMATRIX.equals(reader.getName())); 602 event = reader.next()) { 603 if (event == XMLStreamReader.START_ELEMENT) { 604 if (GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER.equals(reader.getName())) { 605 ret.identifier = reader.getElementText(); 606 } 607 if (QN_SCALE_DENOMINATOR.equals(reader.getName())) { 608 ret.scaleDenominator = Double.parseDouble(reader.getElementText()); 609 } 610 if (QN_TOPLEFT_CORNER.equals(reader.getName())) { 611 String[] topLeftCorner = reader.getElementText().split(" "); 612 if (matrixProj.switchXY()) { 613 ret.topLeftCorner = new EastNorth(Double.parseDouble(topLeftCorner[1]), Double.parseDouble(topLeftCorner[0])); 614 } else { 615 ret.topLeftCorner = new EastNorth(Double.parseDouble(topLeftCorner[0]), Double.parseDouble(topLeftCorner[1])); 616 } 617 } 618 if (QN_TILE_HEIGHT.equals(reader.getName())) { 619 ret.tileHeight = Integer.parseInt(reader.getElementText()); 620 } 621 if (QN_TILE_WIDTH.equals(reader.getName())) { 622 ret.tileWidth = Integer.parseInt(reader.getElementText()); 623 } 624 if (QN_MATRIX_HEIGHT.equals(reader.getName())) { 625 ret.matrixHeight = Integer.parseInt(reader.getElementText()); 626 } 627 if (QN_MATRIX_WIDTH.equals(reader.getName())) { 628 ret.matrixWidth = Integer.parseInt(reader.getElementText()); 629 } 630 } 631 } 632 if (ret.tileHeight != ret.tileWidth) { 633 throw new AssertionError(tr("Only square tiles are supported. {0}x{1} returned by server for TileMatrix identifier {2}", 634 ret.tileHeight, ret.tileWidth, ret.identifier)); 635 } 636 return ret; 637 } 638 639 /** 640 * Parses OperationMetadata section. Returns when reader is on OperationsMetadata closing tag. 641 * Sets this.baseUrl and this.transferMode 642 * 643 * @param reader StAX reader instance 644 * @throws XMLStreamException See {@link XMLStreamReader} 645 */ 646 private void parseOperationMetadata(XMLStreamReader reader) throws XMLStreamException { 647 for (int event = reader.getEventType(); 648 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && 649 GetCapabilitiesParseHelper.QN_OWS_OPERATIONS_METADATA.equals(reader.getName())); 650 event = reader.next()) { 651 if (event == XMLStreamReader.START_ELEMENT && 652 GetCapabilitiesParseHelper.QN_OWS_OPERATION.equals(reader.getName()) && 653 "GetTile".equals(reader.getAttributeValue("", "name")) && 654 GetCapabilitiesParseHelper.moveReaderToTag(reader, 655 GetCapabilitiesParseHelper.QN_OWS_DCP, 656 GetCapabilitiesParseHelper.QN_OWS_HTTP, 657 GetCapabilitiesParseHelper.QN_OWS_GET 658 )) { 659 this.baseUrl = reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href"); 660 this.transferMode = GetCapabilitiesParseHelper.getTransferMode(reader); 661 } 662 } 663 } 664 665 /** 666 * Initializes projection for this TileSource with projection 667 * @param proj projection to be used by this TileSource 668 */ 669 public void initProjection(Projection proj) { 670 if (proj.equals(tileProjection)) 671 return; 672 List<Layer> matchingLayers = layers.stream().filter( 673 l -> l.identifier.equals(defaultLayer.layerName) && l.tileMatrixSet.crs.equals(proj.toCode())) 674 .collect(Collectors.toList()); 675 if (matchingLayers.size() > 1) { 676 this.currentLayer = matchingLayers.stream().filter( 677 l -> l.tileMatrixSet.identifier.equals(defaultLayer.getTileMatrixSet())) 678 .findFirst().orElse(matchingLayers.get(0)); 679 this.tileProjection = proj; 680 } else if (matchingLayers.size() == 1) { 681 this.currentLayer = matchingLayers.get(0); 682 this.tileProjection = proj; 683 } else { 684 // no tile matrix sets with current projection 685 if (this.currentLayer == null) { 686 this.tileProjection = null; 687 for (Layer layer : layers) { 688 if (!layer.identifier.equals(defaultLayer.layerName)) { 689 continue; 690 } 691 Projection pr = Projections.getProjectionByCode(layer.tileMatrixSet.crs); 692 if (pr != null) { 693 this.currentLayer = layer; 694 this.tileProjection = pr; 695 break; 696 } 697 } 698 if (this.currentLayer == null) 699 throw new IllegalArgumentException( 700 layers.stream().map(l -> l.tileMatrixSet).collect(Collectors.toList()).toString()); 701 } // else: keep currentLayer and tileProjection as is 702 } 703 if (this.currentLayer != null) { 704 this.currentTileMatrixSet = this.currentLayer.tileMatrixSet; 705 Collection<Double> scales = new ArrayList<>(currentTileMatrixSet.tileMatrix.size()); 706 for (TileMatrix tileMatrix : currentTileMatrixSet.tileMatrix) { 707 scales.add(tileMatrix.scaleDenominator * 0.28e-03); 708 } 709 this.nativeScaleList = new ScaleList(scales); 710 } 711 this.crsScale = getTileSize() * 0.28e-03 / this.tileProjection.getMetersPerUnit(); 712 } 713 714 @Override 715 public int getTileSize() { 716 if (cachedTileSize > 0) { 717 return cachedTileSize; 718 } 719 if (currentTileMatrixSet != null) { 720 // no support for non-square tiles (tileHeight != tileWidth) 721 // and for different tile sizes at different zoom levels 722 cachedTileSize = currentTileMatrixSet.tileMatrix.get(0).tileHeight; 723 return cachedTileSize; 724 } 725 // Fallback to default mercator tile size. Maybe it will work 726 Logging.warn("WMTS: Could not determine tile size. Using default tile size of: {0}", getDefaultTileSize()); 727 return getDefaultTileSize(); 728 } 729 730 @Override 731 public String getTileUrl(int zoom, int tilex, int tiley) { 732 if (currentLayer == null) { 733 return ""; 734 } 735 736 String url; 737 if (currentLayer.baseUrl != null && transferMode == null) { 738 url = currentLayer.baseUrl; 739 } else { 740 switch (transferMode) { 741 case KVP: 742 url = baseUrl + URL_GET_ENCODING_PARAMS; 743 break; 744 case REST: 745 url = currentLayer.baseUrl; 746 break; 747 default: 748 url = ""; 749 break; 750 } 751 } 752 753 TileMatrix tileMatrix = getTileMatrix(zoom); 754 755 if (tileMatrix == null) { 756 return ""; // no matrix, probably unsupported CRS selected. 757 } 758 759 url = url.replaceAll("\\{layer\\}", this.currentLayer.identifier) 760 .replaceAll("\\{format\\}", this.currentLayer.format) 761 .replaceAll("\\{TileMatrixSet\\}", this.currentTileMatrixSet.identifier) 762 .replaceAll("\\{TileMatrix\\}", tileMatrix.identifier) 763 .replaceAll("\\{TileRow\\}", Integer.toString(tiley)) 764 .replaceAll("\\{TileCol\\}", Integer.toString(tilex)) 765 .replaceAll("(?i)\\{style\\}", this.currentLayer.style); 766 767 for (Dimension d : currentLayer.dimensions) { 768 url = url.replaceAll("(?i)\\{"+d.identifier+"\\}", d.defaultValue); 769 } 770 771 return url; 772 } 773 774 /** 775 * 776 * @param zoom zoom level 777 * @return TileMatrix that's working on this zoom level 778 */ 779 private TileMatrix getTileMatrix(int zoom) { 780 if (zoom > getMaxZoom()) { 781 return null; 782 } 783 if (zoom < 0) { 784 return null; 785 } 786 return this.currentTileMatrixSet.tileMatrix.get(zoom); 787 } 788 789 @Override 790 public double getDistance(double lat1, double lon1, double lat2, double lon2) { 791 throw new UnsupportedOperationException("Not implemented"); 792 } 793 794 @Override 795 public ICoordinate tileXYToLatLon(Tile tile) { 796 return tileXYToLatLon(tile.getXtile(), tile.getYtile(), tile.getZoom()); 797 } 798 799 @Override 800 public ICoordinate tileXYToLatLon(TileXY xy, int zoom) { 801 return tileXYToLatLon(xy.getXIndex(), xy.getYIndex(), zoom); 802 } 803 804 @Override 805 public ICoordinate tileXYToLatLon(int x, int y, int zoom) { 806 TileMatrix matrix = getTileMatrix(zoom); 807 if (matrix == null) { 808 return CoordinateConversion.llToCoor(tileProjection.getWorldBoundsLatLon().getCenter()); 809 } 810 double scale = matrix.scaleDenominator * this.crsScale; 811 EastNorth ret = new EastNorth(matrix.topLeftCorner.east() + x * scale, matrix.topLeftCorner.north() - y * scale); 812 return CoordinateConversion.llToCoor(tileProjection.eastNorth2latlon(ret)); 813 } 814 815 @Override 816 public TileXY latLonToTileXY(double lat, double lon, int zoom) { 817 TileMatrix matrix = getTileMatrix(zoom); 818 if (matrix == null) { 819 return new TileXY(0, 0); 820 } 821 822 EastNorth enPoint = tileProjection.latlon2eastNorth(new LatLon(lat, lon)); 823 double scale = matrix.scaleDenominator * this.crsScale; 824 return new TileXY( 825 (enPoint.east() - matrix.topLeftCorner.east()) / scale, 826 (matrix.topLeftCorner.north() - enPoint.north()) / scale 827 ); 828 } 829 830 @Override 831 public TileXY latLonToTileXY(ICoordinate point, int zoom) { 832 return latLonToTileXY(point.getLat(), point.getLon(), zoom); 833 } 834 835 @Override 836 public int getTileXMax(int zoom) { 837 return getTileXMax(zoom, tileProjection); 838 } 839 840 @Override 841 public int getTileYMax(int zoom) { 842 return getTileYMax(zoom, tileProjection); 843 } 844 845 @Override 846 public Point latLonToXY(double lat, double lon, int zoom) { 847 TileMatrix matrix = getTileMatrix(zoom); 848 if (matrix == null) { 849 return new Point(0, 0); 850 } 851 double scale = matrix.scaleDenominator * this.crsScale; 852 EastNorth point = tileProjection.latlon2eastNorth(new LatLon(lat, lon)); 853 return new Point( 854 (int) Math.round((point.east() - matrix.topLeftCorner.east()) / scale), 855 (int) Math.round((matrix.topLeftCorner.north() - point.north()) / scale) 856 ); 857 } 858 859 @Override 860 public Point latLonToXY(ICoordinate point, int zoom) { 861 return latLonToXY(point.getLat(), point.getLon(), zoom); 862 } 863 864 @Override 865 public Coordinate xyToLatLon(Point point, int zoom) { 866 return xyToLatLon(point.x, point.y, zoom); 867 } 868 869 @Override 870 public Coordinate xyToLatLon(int x, int y, int zoom) { 871 TileMatrix matrix = getTileMatrix(zoom); 872 if (matrix == null) { 873 return new Coordinate(0, 0); 874 } 875 double scale = matrix.scaleDenominator * this.crsScale; 876 EastNorth ret = new EastNorth( 877 matrix.topLeftCorner.east() + x * scale, 878 matrix.topLeftCorner.north() - y * scale 879 ); 880 LatLon ll = tileProjection.eastNorth2latlon(ret); 881 return new Coordinate(ll.lat(), ll.lon()); 882 } 883 884 @Override 885 public Map<String, String> getHeaders() { 886 return headers; 887 } 888 889 @Override 890 public int getMaxZoom() { 891 if (this.currentTileMatrixSet != null) { 892 return this.currentTileMatrixSet.tileMatrix.size()-1; 893 } 894 return 0; 895 } 896 897 @Override 898 public String getTileId(int zoom, int tilex, int tiley) { 899 return getTileUrl(zoom, tilex, tiley); 900 } 901 902 /** 903 * Checks if url is acceptable by this Tile Source 904 * @param url URL to check 905 */ 906 public static void checkUrl(String url) { 907 CheckParameterUtil.ensureParameterNotNull(url, "url"); 908 Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url); 909 while (m.find()) { 910 boolean isSupportedPattern = false; 911 for (String pattern : ALL_PATTERNS) { 912 if (m.group().matches(pattern)) { 913 isSupportedPattern = true; 914 break; 915 } 916 } 917 if (!isSupportedPattern) { 918 throw new IllegalArgumentException( 919 tr("{0} is not a valid WMS argument. Please check this server URL:\n{1}", m.group(), url)); 920 } 921 } 922 } 923 924 /** 925 * @return set of projection codes that this TileSource supports 926 */ 927 public Collection<String> getSupportedProjections() { 928 Collection<String> ret = new LinkedHashSet<>(); 929 if (currentLayer == null) { 930 for (Layer layer: this.layers) { 931 ret.add(layer.tileMatrixSet.crs); 932 } 933 } else { 934 for (Layer layer: this.layers) { 935 if (currentLayer.identifier.equals(layer.identifier)) { 936 ret.add(layer.tileMatrixSet.crs); 937 } 938 } 939 } 940 return ret; 941 } 942 943 private int getTileYMax(int zoom, Projection proj) { 944 TileMatrix matrix = getTileMatrix(zoom); 945 if (matrix == null) { 946 return 0; 947 } 948 949 if (matrix.matrixHeight != -1) { 950 return matrix.matrixHeight; 951 } 952 953 double scale = matrix.scaleDenominator * this.crsScale; 954 EastNorth min = matrix.topLeftCorner; 955 EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax()); 956 return (int) Math.ceil(Math.abs(max.north() - min.north()) / scale); 957 } 958 959 private int getTileXMax(int zoom, Projection proj) { 960 TileMatrix matrix = getTileMatrix(zoom); 961 if (matrix == null) { 962 return 0; 963 } 964 if (matrix.matrixWidth != -1) { 965 return matrix.matrixWidth; 966 } 967 968 double scale = matrix.scaleDenominator * this.crsScale; 969 EastNorth min = matrix.topLeftCorner; 970 EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax()); 971 return (int) Math.ceil(Math.abs(max.east() - min.east()) / scale); 972 } 973 974 /** 975 * Get native scales of tile source. 976 * @return {@link ScaleList} of native scales 977 */ 978 public ScaleList getNativeScales() { 979 return nativeScaleList; 980 } 981 982 /** 983 * Returns the tile projection. 984 * @return the tile projection 985 */ 986 public Projection getTileProjection() { 987 return tileProjection; 988 } 989 990 @Override 991 public IProjected tileXYtoProjected(int x, int y, int zoom) { 992 TileMatrix matrix = getTileMatrix(zoom); 993 if (matrix == null) { 994 return new Projected(0, 0); 995 } 996 double scale = matrix.scaleDenominator * this.crsScale; 997 return new Projected( 998 matrix.topLeftCorner.east() + x * scale, 999 matrix.topLeftCorner.north() - y * scale); 1000 } 1001 1002 @Override 1003 public TileXY projectedToTileXY(IProjected projected, int zoom) { 1004 TileMatrix matrix = getTileMatrix(zoom); 1005 if (matrix == null) { 1006 return new TileXY(0, 0); 1007 } 1008 double scale = matrix.scaleDenominator * this.crsScale; 1009 return new TileXY( 1010 (projected.getEast() - matrix.topLeftCorner.east()) / scale, 1011 -(projected.getNorth() - matrix.topLeftCorner.north()) / scale); 1012 } 1013 1014 private EastNorth tileToEastNorth(int x, int y, int z) { 1015 return CoordinateConversion.projToEn(this.tileXYtoProjected(x, y, z)); 1016 } 1017 1018 private ProjectionBounds getTileProjectionBounds(Tile tile) { 1019 ProjectionBounds pb = new ProjectionBounds(tileToEastNorth(tile.getXtile(), tile.getYtile(), tile.getZoom())); 1020 pb.extend(tileToEastNorth(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom())); 1021 return pb; 1022 } 1023 1024 @Override 1025 public boolean isInside(Tile inner, Tile outer) { 1026 ProjectionBounds pbInner = getTileProjectionBounds(inner); 1027 ProjectionBounds pbOuter = getTileProjectionBounds(outer); 1028 // a little tolerance, for when inner tile touches the border of the outer tile 1029 double epsilon = 1e-7 * (pbOuter.maxEast - pbOuter.minEast); 1030 return pbOuter.minEast <= pbInner.minEast + epsilon && 1031 pbOuter.minNorth <= pbInner.minNorth + epsilon && 1032 pbOuter.maxEast >= pbInner.maxEast - epsilon && 1033 pbOuter.maxNorth >= pbInner.maxNorth - epsilon; 1034 } 1035 1036 @Override 1037 public TileRange getCoveringTileRange(Tile tile, int newZoom) { 1038 TileMatrix matrixNew = getTileMatrix(newZoom); 1039 if (matrixNew == null) { 1040 return new TileRange(new TileXY(0, 0), new TileXY(0, 0), newZoom); 1041 } 1042 IProjected p0 = tileXYtoProjected(tile.getXtile(), tile.getYtile(), tile.getZoom()); 1043 IProjected p1 = tileXYtoProjected(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom()); 1044 TileXY tMin = projectedToTileXY(p0, newZoom); 1045 TileXY tMax = projectedToTileXY(p1, newZoom); 1046 // shrink the target tile a little, so we don't get neighboring tiles, that 1047 // share an edge, but don't actually cover the target tile 1048 double epsilon = 1e-7 * (tMax.getX() - tMin.getX()); 1049 int minX = (int) Math.floor(tMin.getX() + epsilon); 1050 int minY = (int) Math.floor(tMin.getY() + epsilon); 1051 int maxX = (int) Math.ceil(tMax.getX() - epsilon) - 1; 1052 int maxY = (int) Math.ceil(tMax.getY() - epsilon) - 1; 1053 return new TileRange(new TileXY(minX, minY), new TileXY(maxX, maxY), newZoom); 1054 } 1055 1056 @Override 1057 public String getServerCRS() { 1058 return tileProjection != null ? tileProjection.toCode() : null; 1059 } 1060}