| 1 | // License: GPL. For details, see LICENSE file.
|
|---|
| 2 | package org.openstreetmap.josm.data.imagery;
|
|---|
| 3 |
|
|---|
| 4 | import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
|
|---|
| 5 | import static javax.xml.stream.XMLStreamConstants.START_ELEMENT;
|
|---|
| 6 | import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_DCP;
|
|---|
| 7 | import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_GET;
|
|---|
| 8 | import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_HTTP;
|
|---|
| 9 | import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_IDENTIFIER;
|
|---|
| 10 | import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_LOWER_CORNER;
|
|---|
| 11 | import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_OPERATION;
|
|---|
| 12 | import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_OPERATIONS_METADATA;
|
|---|
| 13 | import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_SUPPORTED_CRS;
|
|---|
| 14 | import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_TITLE;
|
|---|
| 15 | import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_UPPER_CORNER;
|
|---|
| 16 | import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_WGS84_BOUNDING_BOX;
|
|---|
| 17 | import static org.openstreetmap.josm.tools.I18n.tr;
|
|---|
| 18 |
|
|---|
| 19 | import java.awt.Point;
|
|---|
| 20 | import java.io.ByteArrayInputStream;
|
|---|
| 21 | import java.io.IOException;
|
|---|
| 22 | import java.io.InputStream;
|
|---|
| 23 | import java.nio.charset.StandardCharsets;
|
|---|
| 24 | import java.nio.file.InvalidPathException;
|
|---|
| 25 | import java.util.ArrayList;
|
|---|
| 26 | import java.util.Arrays;
|
|---|
| 27 | import java.util.Collection;
|
|---|
| 28 | import java.util.Collections;
|
|---|
| 29 | import java.util.Deque;
|
|---|
| 30 | import java.util.HashMap;
|
|---|
| 31 | import java.util.LinkedHashSet;
|
|---|
| 32 | import java.util.LinkedList;
|
|---|
| 33 | import java.util.List;
|
|---|
| 34 | import java.util.Map;
|
|---|
| 35 | import java.util.Map.Entry;
|
|---|
| 36 | import java.util.Objects;
|
|---|
| 37 | import java.util.Optional;
|
|---|
| 38 | import java.util.SortedSet;
|
|---|
| 39 | import java.util.TreeSet;
|
|---|
| 40 | import java.util.concurrent.ConcurrentHashMap;
|
|---|
| 41 | import java.util.function.BiFunction;
|
|---|
| 42 | import java.util.stream.Collectors;
|
|---|
| 43 |
|
|---|
| 44 | import javax.imageio.ImageIO;
|
|---|
| 45 | import javax.swing.ListSelectionModel;
|
|---|
| 46 | import javax.xml.namespace.QName;
|
|---|
| 47 | import javax.xml.stream.XMLStreamException;
|
|---|
| 48 | import javax.xml.stream.XMLStreamReader;
|
|---|
| 49 |
|
|---|
| 50 | import org.openstreetmap.gui.jmapviewer.Coordinate;
|
|---|
| 51 | import org.openstreetmap.gui.jmapviewer.Projected;
|
|---|
| 52 | import org.openstreetmap.gui.jmapviewer.Tile;
|
|---|
| 53 | import org.openstreetmap.gui.jmapviewer.TileRange;
|
|---|
| 54 | import org.openstreetmap.gui.jmapviewer.TileXY;
|
|---|
| 55 | import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
|
|---|
| 56 | import org.openstreetmap.gui.jmapviewer.interfaces.IProjected;
|
|---|
| 57 | import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
|
|---|
| 58 | import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
|
|---|
| 59 | import org.openstreetmap.josm.data.ProjectionBounds;
|
|---|
| 60 | import org.openstreetmap.josm.data.coor.EastNorth;
|
|---|
| 61 | import org.openstreetmap.josm.data.coor.LatLon;
|
|---|
| 62 | import org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.TransferMode;
|
|---|
| 63 | import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
|
|---|
| 64 | import org.openstreetmap.josm.data.osm.BBox;
|
|---|
| 65 | import org.openstreetmap.josm.data.projection.Projection;
|
|---|
| 66 | import org.openstreetmap.josm.data.projection.ProjectionRegistry;
|
|---|
| 67 | import org.openstreetmap.josm.data.projection.Projections;
|
|---|
| 68 | import org.openstreetmap.josm.gui.ExtendedDialog;
|
|---|
| 69 | import org.openstreetmap.josm.gui.MainApplication;
|
|---|
| 70 | import org.openstreetmap.josm.gui.layer.NativeScaleLayer.ScaleList;
|
|---|
| 71 | import org.openstreetmap.josm.gui.layer.imagery.WMTSLayerSelection;
|
|---|
| 72 | import org.openstreetmap.josm.io.CachedFile;
|
|---|
| 73 | import org.openstreetmap.josm.spi.preferences.Config;
|
|---|
| 74 | import org.openstreetmap.josm.tools.CheckParameterUtil;
|
|---|
| 75 | import org.openstreetmap.josm.tools.Logging;
|
|---|
| 76 |
|
|---|
| 77 | /**
|
|---|
| 78 | * Tile Source handling WMTS providers
|
|---|
| 79 | *
|
|---|
| 80 | * @author Wiktor Niesiobędzki
|
|---|
| 81 | * @since 8526
|
|---|
| 82 | */
|
|---|
| 83 | public class WMTSTileSource extends AbstractTMSTileSource implements TemplatedTileSource {
|
|---|
| 84 | /**
|
|---|
| 85 | * WMTS namespace address
|
|---|
| 86 | */
|
|---|
| 87 | public static final String WMTS_NS_URL = "http://www.opengis.net/wmts/1.0";
|
|---|
| 88 |
|
|---|
| 89 | // CHECKSTYLE.OFF: SingleSpaceSeparator
|
|---|
| 90 | private static final QName QN_CONTENTS = new QName(WMTS_NS_URL, "Contents");
|
|---|
| 91 | private static final QName QN_DEFAULT = new QName(WMTS_NS_URL, "Default");
|
|---|
| 92 | private static final QName QN_DIMENSION = new QName(WMTS_NS_URL, "Dimension");
|
|---|
| 93 | private static final QName QN_FORMAT = new QName(WMTS_NS_URL, "Format");
|
|---|
| 94 | private static final QName QN_LAYER = new QName(WMTS_NS_URL, "Layer");
|
|---|
| 95 | private static final QName QN_MATRIX_WIDTH = new QName(WMTS_NS_URL, "MatrixWidth");
|
|---|
| 96 | private static final QName QN_MATRIX_HEIGHT = new QName(WMTS_NS_URL, "MatrixHeight");
|
|---|
| 97 | private static final QName QN_RESOURCE_URL = new QName(WMTS_NS_URL, "ResourceURL");
|
|---|
| 98 | private static final QName QN_SCALE_DENOMINATOR = new QName(WMTS_NS_URL, "ScaleDenominator");
|
|---|
| 99 | private static final QName QN_STYLE = new QName(WMTS_NS_URL, "Style");
|
|---|
| 100 | private static final QName QN_TILEMATRIX = new QName(WMTS_NS_URL, "TileMatrix");
|
|---|
| 101 | private static final QName QN_TILEMATRIXSET = new QName(WMTS_NS_URL, "TileMatrixSet");
|
|---|
| 102 | private static final QName QN_TILEMATRIX_SET_LINK = new QName(WMTS_NS_URL, "TileMatrixSetLink");
|
|---|
| 103 | private static final QName QN_TILE_WIDTH = new QName(WMTS_NS_URL, "TileWidth");
|
|---|
| 104 | private static final QName QN_TILE_HEIGHT = new QName(WMTS_NS_URL, "TileHeight");
|
|---|
| 105 | private static final QName QN_TOPLEFT_CORNER = new QName(WMTS_NS_URL, "TopLeftCorner");
|
|---|
| 106 | private static final QName QN_VALUE = new QName(WMTS_NS_URL, "Value");
|
|---|
| 107 | // CHECKSTYLE.ON: SingleSpaceSeparator
|
|---|
| 108 |
|
|---|
| 109 | private static final String URL_GET_ENCODING_PARAMS = "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER={layer}&STYLE={style}&"
|
|---|
| 110 | + "FORMAT={format}&tileMatrixSet={TileMatrixSet}&tileMatrix={TileMatrix}&tileRow={TileRow}&tileCol={TileCol}";
|
|---|
| 111 |
|
|---|
| 112 | private int cachedTileSize = -1;
|
|---|
| 113 |
|
|---|
| 114 | private static final class TileMatrix {
|
|---|
| 115 | private String identifier;
|
|---|
| 116 | private double scaleDenominator;
|
|---|
| 117 | private EastNorth topLeftCorner;
|
|---|
| 118 | private int tileWidth;
|
|---|
| 119 | private int tileHeight;
|
|---|
| 120 | private int matrixWidth = -1;
|
|---|
| 121 | private int matrixHeight = -1;
|
|---|
| 122 | }
|
|---|
| 123 |
|
|---|
| 124 | private static final class TileMatrixSetBuilder {
|
|---|
| 125 | // sorted by zoom level
|
|---|
| 126 | SortedSet<TileMatrix> tileMatrix = new TreeSet<>((o1, o2) -> -1 * Double.compare(o1.scaleDenominator, o2.scaleDenominator));
|
|---|
| 127 | private String crs;
|
|---|
| 128 | private String identifier;
|
|---|
| 129 |
|
|---|
| 130 | TileMatrixSet build() {
|
|---|
| 131 | return new TileMatrixSet(this);
|
|---|
| 132 | }
|
|---|
| 133 | }
|
|---|
| 134 |
|
|---|
| 135 | /**
|
|---|
| 136 | * class representing WMTS TileMatrixSet
|
|---|
| 137 | * This connects projection and TileMatrix (how the map is divided in tiles)
|
|---|
| 138 | * @since 13733
|
|---|
| 139 | */
|
|---|
| 140 | public static class TileMatrixSet {
|
|---|
| 141 |
|
|---|
| 142 | private final List<TileMatrix> tileMatrix;
|
|---|
| 143 | private final String crs;
|
|---|
| 144 | private final String identifier;
|
|---|
| 145 |
|
|---|
| 146 | TileMatrixSet(TileMatrixSet tileMatrixSet) {
|
|---|
| 147 | if (tileMatrixSet != null) {
|
|---|
| 148 | tileMatrix = new ArrayList<>(tileMatrixSet.tileMatrix);
|
|---|
| 149 | crs = tileMatrixSet.crs;
|
|---|
| 150 | identifier = tileMatrixSet.identifier;
|
|---|
| 151 | } else {
|
|---|
| 152 | tileMatrix = Collections.emptyList();
|
|---|
| 153 | crs = null;
|
|---|
| 154 | identifier = null;
|
|---|
| 155 | }
|
|---|
| 156 | }
|
|---|
| 157 |
|
|---|
| 158 | TileMatrixSet(TileMatrixSetBuilder builder) {
|
|---|
| 159 | tileMatrix = new ArrayList<>(builder.tileMatrix);
|
|---|
| 160 | crs = builder.crs;
|
|---|
| 161 | identifier = builder.identifier;
|
|---|
| 162 | }
|
|---|
| 163 |
|
|---|
| 164 | @Override
|
|---|
| 165 | public String toString() {
|
|---|
| 166 | return "TileMatrixSet [crs=" + crs + ", identifier=" + identifier + ']';
|
|---|
| 167 | }
|
|---|
| 168 |
|
|---|
| 169 | /**
|
|---|
| 170 | * Returns identifier of this TileMatrixSet.
|
|---|
| 171 | * @return identifier of this TileMatrixSet
|
|---|
| 172 | */
|
|---|
| 173 | public String getIdentifier() {
|
|---|
| 174 | return identifier;
|
|---|
| 175 | }
|
|---|
| 176 |
|
|---|
| 177 | /**
|
|---|
| 178 | * Returns projection of this tileMatrix.
|
|---|
| 179 | * @return projection of this tileMatrix
|
|---|
| 180 | */
|
|---|
| 181 | public String getCrs() {
|
|---|
| 182 | return crs;
|
|---|
| 183 | }
|
|---|
| 184 |
|
|---|
| 185 | /**
|
|---|
| 186 | * Returns tile matrix max zoom. Assumes first zoom starts at 0, with continuous zoom levels.
|
|---|
| 187 | * @return tile matrix max zoom
|
|---|
| 188 | * @since 15409
|
|---|
| 189 | */
|
|---|
| 190 | public int getMaxZoom() {
|
|---|
| 191 | return tileMatrix.size() - 1;
|
|---|
| 192 | }
|
|---|
| 193 | }
|
|---|
| 194 |
|
|---|
| 195 | private static final class Dimension {
|
|---|
| 196 | private String identifier;
|
|---|
| 197 | private String defaultValue;
|
|---|
| 198 | private final List<String> values = new ArrayList<>();
|
|---|
| 199 | }
|
|---|
| 200 |
|
|---|
| 201 | /**
|
|---|
| 202 | * Class representing WMTS Layer information
|
|---|
| 203 | * @since 13733
|
|---|
| 204 | */
|
|---|
| 205 | public static class Layer {
|
|---|
| 206 | private String format;
|
|---|
| 207 | private String identifier;
|
|---|
| 208 | private String title;
|
|---|
| 209 | private TileMatrixSet tileMatrixSet;
|
|---|
| 210 | private String baseUrl;
|
|---|
| 211 | private String style;
|
|---|
| 212 | private BBox bbox;
|
|---|
| 213 | private final Collection<String> tileMatrixSetLinks = new ArrayList<>();
|
|---|
| 214 | private final Collection<Dimension> dimensions = new ArrayList<>();
|
|---|
| 215 |
|
|---|
| 216 | Layer(Layer l) {
|
|---|
| 217 | Objects.requireNonNull(l);
|
|---|
| 218 | format = l.format;
|
|---|
| 219 | identifier = l.identifier;
|
|---|
| 220 | title = l.title;
|
|---|
| 221 | baseUrl = l.baseUrl;
|
|---|
| 222 | style = l.style;
|
|---|
| 223 | bbox = l.bbox;
|
|---|
| 224 | tileMatrixSet = new TileMatrixSet(l.tileMatrixSet);
|
|---|
| 225 | dimensions.addAll(l.dimensions);
|
|---|
| 226 | }
|
|---|
| 227 |
|
|---|
| 228 | Layer() {
|
|---|
| 229 | }
|
|---|
| 230 |
|
|---|
| 231 | /**
|
|---|
| 232 | * Get title of the layer for user display.
|
|---|
| 233 | * <p>
|
|---|
| 234 | * This is either the content of the Title element (if available) or
|
|---|
| 235 | * the layer identifier (as fallback)
|
|---|
| 236 | * @return title of the layer for user display
|
|---|
| 237 | */
|
|---|
| 238 | public String getUserTitle() {
|
|---|
| 239 | return title != null ? title : identifier;
|
|---|
| 240 | }
|
|---|
| 241 |
|
|---|
| 242 | @Override
|
|---|
| 243 | public String toString() {
|
|---|
| 244 | return "Layer [identifier=" + identifier + ", title=" + title + ", tileMatrixSet="
|
|---|
| 245 | + tileMatrixSet + ", baseUrl=" + baseUrl + ", style=" + style + ']';
|
|---|
| 246 | }
|
|---|
| 247 |
|
|---|
| 248 | /**
|
|---|
| 249 | * Returns identifier of this layer.
|
|---|
| 250 | * @return identifier of this layer
|
|---|
| 251 | */
|
|---|
| 252 | public String getIdentifier() {
|
|---|
| 253 | return identifier;
|
|---|
| 254 | }
|
|---|
| 255 |
|
|---|
| 256 | /**
|
|---|
| 257 | * Returns style of this layer.
|
|---|
| 258 | * @return style of this layer
|
|---|
| 259 | */
|
|---|
| 260 | public String getStyle() {
|
|---|
| 261 | return style;
|
|---|
| 262 | }
|
|---|
| 263 |
|
|---|
| 264 | /**
|
|---|
| 265 | * Returns tileMatrixSet of this layer.
|
|---|
| 266 | * @return tileMatrixSet of this layer
|
|---|
| 267 | */
|
|---|
| 268 | public TileMatrixSet getTileMatrixSet() {
|
|---|
| 269 | return tileMatrixSet;
|
|---|
| 270 | }
|
|---|
| 271 |
|
|---|
| 272 | /**
|
|---|
| 273 | * Returns layer max zoom.
|
|---|
| 274 | * @return layer max zoom
|
|---|
| 275 | * @since 15409
|
|---|
| 276 | */
|
|---|
| 277 | public int getMaxZoom() {
|
|---|
| 278 | return tileMatrixSet != null ? tileMatrixSet.getMaxZoom() : 0;
|
|---|
| 279 | }
|
|---|
| 280 |
|
|---|
| 281 | /**
|
|---|
| 282 | * Returns the WGS84 bounding box.
|
|---|
| 283 | * @return WGS84 bounding box
|
|---|
| 284 | * @since 15410
|
|---|
| 285 | */
|
|---|
| 286 | public BBox getBbox() {
|
|---|
| 287 | return bbox;
|
|---|
| 288 | }
|
|---|
| 289 | }
|
|---|
| 290 |
|
|---|
| 291 | /**
|
|---|
| 292 | * Exception thrown when parser doesn't find expected information in GetCapabilities document
|
|---|
| 293 | * @since 13733
|
|---|
| 294 | */
|
|---|
| 295 | public static class WMTSGetCapabilitiesException extends Exception {
|
|---|
| 296 |
|
|---|
| 297 | /**
|
|---|
| 298 | * Create WMTS exception
|
|---|
| 299 | * @param cause description of cause
|
|---|
| 300 | */
|
|---|
| 301 | public WMTSGetCapabilitiesException(String cause) {
|
|---|
| 302 | super(cause);
|
|---|
| 303 | }
|
|---|
| 304 |
|
|---|
| 305 | /**
|
|---|
| 306 | * Create WMTS exception
|
|---|
| 307 | * @param cause description of cause
|
|---|
| 308 | * @param t nested exception
|
|---|
| 309 | */
|
|---|
| 310 | public WMTSGetCapabilitiesException(String cause, Throwable t) {
|
|---|
| 311 | super(cause, t);
|
|---|
| 312 | }
|
|---|
| 313 | }
|
|---|
| 314 |
|
|---|
| 315 | private static final class SelectLayerDialog extends ExtendedDialog {
|
|---|
| 316 | private final WMTSLayerSelection list;
|
|---|
| 317 |
|
|---|
| 318 | SelectLayerDialog(Collection<Layer> layers) {
|
|---|
| 319 | super(MainApplication.getMainFrame(), tr("Select WMTS layer"), tr("Add layers"), tr("Cancel"));
|
|---|
| 320 | this.list = new WMTSLayerSelection(groupLayersByNameAndTileMatrixSet(layers));
|
|---|
| 321 | setContent(list);
|
|---|
| 322 | }
|
|---|
| 323 |
|
|---|
| 324 | @Override
|
|---|
| 325 | public void setupDialog() {
|
|---|
| 326 | super.setupDialog();
|
|---|
| 327 | buttons.get(0).setEnabled(false);
|
|---|
| 328 | ListSelectionModel selectionModel = list.getTable().getSelectionModel();
|
|---|
| 329 | selectionModel.addListSelectionListener(e -> buttons.get(0).setEnabled(!selectionModel.isSelectionEmpty()));
|
|---|
| 330 | }
|
|---|
| 331 |
|
|---|
| 332 | public DefaultLayer getSelectedLayer() {
|
|---|
| 333 | Layer selectedLayer = list.getSelectedLayer();
|
|---|
| 334 | return selectedLayer == null ? null :
|
|---|
| 335 | new DefaultLayer(ImageryType.WMTS, selectedLayer.identifier, selectedLayer.style, selectedLayer.tileMatrixSet.identifier);
|
|---|
| 336 | }
|
|---|
| 337 | }
|
|---|
| 338 |
|
|---|
| 339 | private final Map<String, String> headers = new ConcurrentHashMap<>();
|
|---|
| 340 | private final Collection<Layer> layers;
|
|---|
| 341 | private Layer currentLayer;
|
|---|
| 342 | private TileMatrixSet currentTileMatrixSet;
|
|---|
| 343 | private double crsScale;
|
|---|
| 344 | private final TransferMode transferMode;
|
|---|
| 345 |
|
|---|
| 346 | private ScaleList nativeScaleList;
|
|---|
| 347 |
|
|---|
| 348 | private final DefaultLayer defaultLayer;
|
|---|
| 349 |
|
|---|
| 350 | private Projection tileProjection;
|
|---|
| 351 |
|
|---|
| 352 | /**
|
|---|
| 353 | * Creates a tile source based on imagery info
|
|---|
| 354 | * @param info imagery info
|
|---|
| 355 | * @throws IOException if any I/O error occurs
|
|---|
| 356 | * @throws WMTSGetCapabilitiesException when document didn't contain any layers
|
|---|
| 357 | * @throws IllegalArgumentException if any other error happens for the given imagery info
|
|---|
| 358 | */
|
|---|
| 359 | public WMTSTileSource(ImageryInfo info) throws IOException, WMTSGetCapabilitiesException {
|
|---|
| 360 | super(info);
|
|---|
| 361 | CheckParameterUtil.ensureThat(info.getDefaultLayers().size() < 2, "At most 1 default layer for WMTS is supported");
|
|---|
| 362 | this.headers.putAll(info.getCustomHttpHeaders());
|
|---|
| 363 | this.baseUrl = GetCapabilitiesParseHelper.normalizeCapabilitiesUrl(
|
|---|
| 364 | ImageryPatterns.handleApiKeyTemplate(info.getId(), ImageryPatterns.handleHeaderTemplate(info.getUrl(), headers)));
|
|---|
| 365 | WMTSCapabilities capabilities = getCapabilities(baseUrl, headers);
|
|---|
| 366 | this.layers = capabilities.getLayers();
|
|---|
| 367 | this.baseUrl = capabilities.getBaseUrl();
|
|---|
| 368 | this.transferMode = capabilities.getTransferMode();
|
|---|
| 369 | if (info.getDefaultLayers().isEmpty()) {
|
|---|
| 370 | Logging.warn(tr("No default layer selected, choosing first layer."));
|
|---|
| 371 | if (!layers.isEmpty()) {
|
|---|
| 372 | Layer first = layers.iterator().next();
|
|---|
| 373 | // If max zoom lower than expected, try to find a better layer
|
|---|
| 374 | final int maxZoom = info.getMaxZoom();
|
|---|
| 375 | if (first.getMaxZoom() < maxZoom) {
|
|---|
| 376 | first = layers.stream().filter(l -> l.getMaxZoom() >= maxZoom).findFirst().orElse(first);
|
|---|
| 377 | }
|
|---|
| 378 | // If center of josm bbox not in layer bbox, try to find a better layer
|
|---|
| 379 | if (info.getBounds() != null && first.getBbox() != null) {
|
|---|
| 380 | LatLon center = info.getBounds().getCenter();
|
|---|
| 381 | if (!first.getBbox().bounds(center)) {
|
|---|
| 382 | final Layer ffirst = first;
|
|---|
| 383 | first = layers.stream()
|
|---|
| 384 | .filter(l -> l.getMaxZoom() >= maxZoom && l.getBbox() != null && l.getBbox().bounds(center)).findFirst()
|
|---|
| 385 | .orElseGet(() -> layers.stream().filter(l -> l.getBbox() != null && l.getBbox().bounds(center)).findFirst()
|
|---|
| 386 | .orElse(ffirst));
|
|---|
| 387 | }
|
|---|
| 388 | }
|
|---|
| 389 | this.defaultLayer = new DefaultLayer(info.getImageryType(), first.identifier, first.style, first.tileMatrixSet.identifier);
|
|---|
| 390 | } else {
|
|---|
| 391 | this.defaultLayer = null;
|
|---|
| 392 | }
|
|---|
| 393 | } else {
|
|---|
| 394 | this.defaultLayer = info.getDefaultLayers().get(0);
|
|---|
| 395 | }
|
|---|
| 396 | if (this.layers.isEmpty())
|
|---|
| 397 | throw new IllegalArgumentException(tr("No layers defined by getCapabilities document: {0}", info.getUrl()));
|
|---|
| 398 | }
|
|---|
| 399 |
|
|---|
| 400 | /**
|
|---|
| 401 | * Creates a tile source based on imagery info and initializes it with given projection.
|
|---|
| 402 | * @param info imagery info
|
|---|
| 403 | * @param projection projection to be used by this TileSource
|
|---|
| 404 | * @throws IOException if any I/O error occurs
|
|---|
| 405 | * @throws WMTSGetCapabilitiesException when document didn't contain any layers
|
|---|
| 406 | * @throws IllegalArgumentException if any other error happens for the given imagery info
|
|---|
| 407 | * @since 14507
|
|---|
| 408 | */
|
|---|
| 409 | public WMTSTileSource(ImageryInfo info, Projection projection) throws IOException, WMTSGetCapabilitiesException {
|
|---|
| 410 | this(info);
|
|---|
| 411 | initProjection(projection);
|
|---|
| 412 | }
|
|---|
| 413 |
|
|---|
| 414 | /**
|
|---|
| 415 | * Creates a dialog based on this tile source with all available layers and returns the name of selected layer
|
|---|
| 416 | * @return Name of selected layer
|
|---|
| 417 | */
|
|---|
| 418 | public DefaultLayer userSelectLayer() {
|
|---|
| 419 | Map<String, List<Layer>> layerById = layers.stream().collect(
|
|---|
| 420 | Collectors.groupingBy(x -> x.identifier));
|
|---|
| 421 | if (layerById.size() == 1) { // only one layer
|
|---|
| 422 | List<Layer> ls = layerById.entrySet().iterator().next().getValue()
|
|---|
| 423 | .stream().filter(
|
|---|
| 424 | u -> u.tileMatrixSet.crs.equals(ProjectionRegistry.getProjection().toCode()))
|
|---|
| 425 | .collect(Collectors.toList());
|
|---|
| 426 | if (ls.size() == 1) {
|
|---|
| 427 | // only one tile matrix set with matching projection - no point in asking
|
|---|
| 428 | Layer selectedLayer = ls.get(0);
|
|---|
| 429 | return new DefaultLayer(ImageryType.WMTS, selectedLayer.identifier, selectedLayer.style, selectedLayer.tileMatrixSet.identifier);
|
|---|
| 430 | }
|
|---|
| 431 | }
|
|---|
| 432 |
|
|---|
| 433 | final SelectLayerDialog layerSelection = new SelectLayerDialog(layers);
|
|---|
| 434 | if (layerSelection.showDialog().getValue() == 1) {
|
|---|
| 435 | return layerSelection.getSelectedLayer();
|
|---|
| 436 | }
|
|---|
| 437 | return null;
|
|---|
| 438 | }
|
|---|
| 439 |
|
|---|
| 440 | /**
|
|---|
| 441 | * Call remote server and parse response to WMTSCapabilities object
|
|---|
| 442 | *
|
|---|
| 443 | * @param url of the getCapabilities document
|
|---|
| 444 | * @param headers HTTP headers to set when calling getCapabilities url
|
|---|
| 445 | * @return capabilities
|
|---|
| 446 | * @throws IOException in case of any I/O error
|
|---|
| 447 | * @throws WMTSGetCapabilitiesException when document didn't contain any layers
|
|---|
| 448 | * @throws IllegalArgumentException in case of any other error
|
|---|
| 449 | */
|
|---|
| 450 | public static WMTSCapabilities getCapabilities(String url, Map<String, String> headers) throws IOException, WMTSGetCapabilitiesException {
|
|---|
| 451 | try (CachedFile cf = new CachedFile(url); InputStream in = cf.setHttpHeaders(headers).
|
|---|
| 452 | setMaxAge(Config.getPref().getLong("wmts.capabilities.cache.max_age", 7 * CachedFile.DAYS)).
|
|---|
| 453 | setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince).
|
|---|
| 454 | getInputStream()) {
|
|---|
| 455 | byte[] data = in.readAllBytes();
|
|---|
| 456 | if (data.length == 0) {
|
|---|
| 457 | cf.clear();
|
|---|
| 458 | throw new IllegalArgumentException("Could not read data from: " + url);
|
|---|
| 459 | }
|
|---|
| 460 |
|
|---|
| 461 | try {
|
|---|
| 462 | XMLStreamReader reader = GetCapabilitiesParseHelper.getReader(new ByteArrayInputStream(data));
|
|---|
| 463 | WMTSCapabilities ret = null;
|
|---|
| 464 | Collection<Layer> layers = null;
|
|---|
| 465 | for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) {
|
|---|
| 466 | if (event == START_ELEMENT) {
|
|---|
| 467 | QName qName = reader.getName();
|
|---|
| 468 | if (QN_OWS_OPERATIONS_METADATA.equals(qName)) {
|
|---|
| 469 | ret = parseOperationMetadata(reader);
|
|---|
| 470 | } else if (QN_CONTENTS.equals(qName)) {
|
|---|
| 471 | layers = parseContents(reader);
|
|---|
| 472 | }
|
|---|
| 473 | }
|
|---|
| 474 | }
|
|---|
| 475 | if (ret == null) {
|
|---|
| 476 | /*
|
|---|
| 477 | * see #12168 - create dummy operation metadata - not all WMTS services provide this information
|
|---|
| 478 | *
|
|---|
| 479 | * WMTS Standard:
|
|---|
| 480 | * > Resource oriented architecture style HTTP encodings SHALL not be described in the OperationsMetadata section.
|
|---|
| 481 | *
|
|---|
| 482 | * And OperationMetada is not mandatory element. So REST mode is justifiable
|
|---|
| 483 | */
|
|---|
| 484 | ret = new WMTSCapabilities(url, TransferMode.REST);
|
|---|
| 485 | }
|
|---|
| 486 | if (layers == null) {
|
|---|
| 487 | throw new WMTSGetCapabilitiesException(tr("WMTS Capabilities document did not contain layers in url: {0}", url));
|
|---|
| 488 | }
|
|---|
| 489 | ret.addLayers(layers);
|
|---|
| 490 | return ret;
|
|---|
| 491 | } catch (XMLStreamException e) {
|
|---|
| 492 | cf.clear();
|
|---|
| 493 | Logging.warn(new String(data, StandardCharsets.UTF_8));
|
|---|
| 494 | throw new WMTSGetCapabilitiesException(tr("Error during parsing of WMTS Capabilities document: {0}", e.getMessage()), e);
|
|---|
| 495 | }
|
|---|
| 496 | } catch (InvalidPathException e) {
|
|---|
| 497 | throw new WMTSGetCapabilitiesException(tr("Invalid path for GetCapabilities document: {0}", e.getMessage()), e);
|
|---|
| 498 | }
|
|---|
| 499 | }
|
|---|
| 500 |
|
|---|
| 501 | /**
|
|---|
| 502 | * Parse Contents tag. Returns when reader reaches Contents closing tag
|
|---|
| 503 | *
|
|---|
| 504 | * @param reader StAX reader instance
|
|---|
| 505 | * @return collection of layers within contents with properly linked TileMatrixSets
|
|---|
| 506 | * @throws XMLStreamException See {@link XMLStreamReader}
|
|---|
| 507 | */
|
|---|
| 508 | private static Collection<Layer> parseContents(XMLStreamReader reader) throws XMLStreamException {
|
|---|
| 509 | Map<String, TileMatrixSet> matrixSetById = new HashMap<>();
|
|---|
| 510 | Collection<Layer> layers = new ArrayList<>();
|
|---|
| 511 | for (int event = reader.getEventType();
|
|---|
| 512 | reader.hasNext() && !(event == END_ELEMENT && QN_CONTENTS.equals(reader.getName()));
|
|---|
| 513 | event = reader.next()) {
|
|---|
| 514 | if (event == START_ELEMENT) {
|
|---|
| 515 | QName qName = reader.getName();
|
|---|
| 516 | if (QN_LAYER.equals(qName)) {
|
|---|
| 517 | Layer l = parseLayer(reader);
|
|---|
| 518 | if (l != null) {
|
|---|
| 519 | layers.add(l);
|
|---|
| 520 | }
|
|---|
| 521 | } else if (QN_TILEMATRIXSET.equals(qName)) {
|
|---|
| 522 | TileMatrixSet entry = parseTileMatrixSet(reader);
|
|---|
| 523 | matrixSetById.put(entry.identifier, entry);
|
|---|
| 524 | }
|
|---|
| 525 | }
|
|---|
| 526 | }
|
|---|
| 527 | Collection<Layer> ret = new ArrayList<>();
|
|---|
| 528 | // link layers to matrix sets
|
|---|
| 529 | for (Layer l: layers) {
|
|---|
| 530 | for (String tileMatrixId: l.tileMatrixSetLinks) {
|
|---|
| 531 | Layer newLayer = new Layer(l); // create a new layer object for each tile matrix set supported
|
|---|
| 532 | newLayer.tileMatrixSet = matrixSetById.get(tileMatrixId);
|
|---|
| 533 | ret.add(newLayer);
|
|---|
| 534 | }
|
|---|
| 535 | }
|
|---|
| 536 | return ret;
|
|---|
| 537 | }
|
|---|
| 538 |
|
|---|
| 539 | /**
|
|---|
| 540 | * Parse Layer tag. Returns when reader will reach Layer closing tag
|
|---|
| 541 | *
|
|---|
| 542 | * @param reader StAX reader instance
|
|---|
| 543 | * @return Layer object, with tileMatrixSetLinks and no tileMatrixSet attribute set.
|
|---|
| 544 | * @throws XMLStreamException See {@link XMLStreamReader}
|
|---|
| 545 | */
|
|---|
| 546 | private static Layer parseLayer(XMLStreamReader reader) throws XMLStreamException {
|
|---|
| 547 | Layer layer = new Layer();
|
|---|
| 548 | Deque<QName> tagStack = new LinkedList<>();
|
|---|
| 549 | List<String> supportedMimeTypes = new ArrayList<>(Arrays.asList(ImageIO.getReaderMIMETypes()));
|
|---|
| 550 | supportedMimeTypes.add("image/jpgpng"); // used by ESRI
|
|---|
| 551 | supportedMimeTypes.add("image/png8"); // used by geoserver
|
|---|
| 552 | supportedMimeTypes.add("image/vnd.jpeg-png"); // used by geoserver
|
|---|
| 553 | supportedMimeTypes.add("image/vnd.jpeg-png8"); // used by geoserver
|
|---|
| 554 | supportedMimeTypes.add("image/png; mode=8bit"); // used by MapServer
|
|---|
| 555 | if (supportedMimeTypes.contains("image/jpeg")) {
|
|---|
| 556 | supportedMimeTypes.add("image/jpg"); // sometimes misspelled by Arcgis
|
|---|
| 557 | }
|
|---|
| 558 | Collection<String> unsupportedFormats = new ArrayList<>();
|
|---|
| 559 |
|
|---|
| 560 | for (int event = reader.getEventType();
|
|---|
| 561 | reader.hasNext() && !(event == END_ELEMENT && QN_LAYER.equals(reader.getName()));
|
|---|
| 562 | event = reader.next()) {
|
|---|
| 563 | if (event == START_ELEMENT) {
|
|---|
| 564 | QName qName = reader.getName();
|
|---|
| 565 | tagStack.push(qName);
|
|---|
| 566 | if (tagStack.size() == 2) {
|
|---|
| 567 | if (QN_FORMAT.equals(qName)) {
|
|---|
| 568 | String format = reader.getElementText();
|
|---|
| 569 | if (supportedMimeTypes.contains(format)) {
|
|---|
| 570 | layer.format = format;
|
|---|
| 571 | } else {
|
|---|
| 572 | unsupportedFormats.add(format);
|
|---|
| 573 | }
|
|---|
| 574 | } else if (QN_OWS_IDENTIFIER.equals(qName)) {
|
|---|
| 575 | layer.identifier = reader.getElementText();
|
|---|
| 576 | } else if (QN_OWS_TITLE.equals(qName)) {
|
|---|
| 577 | layer.title = reader.getElementText();
|
|---|
| 578 | } else if (QN_RESOURCE_URL.equals(qName) &&
|
|---|
| 579 | "tile".equals(reader.getAttributeValue("", "resourceType"))) {
|
|---|
| 580 | layer.baseUrl = reader.getAttributeValue("", "template");
|
|---|
| 581 | } else if (QN_STYLE.equals(qName) &&
|
|---|
| 582 | "true".equals(reader.getAttributeValue("", "isDefault"))) {
|
|---|
| 583 | if (GetCapabilitiesParseHelper.moveReaderToTag(reader, QN_OWS_IDENTIFIER)) {
|
|---|
| 584 | layer.style = reader.getElementText();
|
|---|
| 585 | tagStack.push(reader.getName()); // keep tagStack in sync
|
|---|
| 586 | }
|
|---|
| 587 | } else if (QN_DIMENSION.equals(qName)) {
|
|---|
| 588 | layer.dimensions.add(parseDimension(reader));
|
|---|
| 589 | } else if (QN_TILEMATRIX_SET_LINK.equals(qName)) {
|
|---|
| 590 | layer.tileMatrixSetLinks.add(parseTileMatrixSetLink(reader));
|
|---|
| 591 | } else if (QN_OWS_WGS84_BOUNDING_BOX.equals(qName)) {
|
|---|
| 592 | layer.bbox = parseBoundingBox(reader);
|
|---|
| 593 | } else {
|
|---|
| 594 | GetCapabilitiesParseHelper.moveReaderToEndCurrentTag(reader);
|
|---|
| 595 | }
|
|---|
| 596 | }
|
|---|
| 597 | }
|
|---|
| 598 | // need to get event type from reader, as parsing might have change position of reader
|
|---|
| 599 | if (reader.getEventType() == END_ELEMENT) {
|
|---|
| 600 | QName start = tagStack.pop();
|
|---|
| 601 | if (!start.equals(reader.getName())) {
|
|---|
| 602 | throw new IllegalStateException(tr("WMTS Parser error - start element {0} has different name than end element {2}",
|
|---|
| 603 | start, reader.getName()));
|
|---|
| 604 | }
|
|---|
| 605 | }
|
|---|
| 606 | }
|
|---|
| 607 | if (layer.style == null) {
|
|---|
| 608 | layer.style = "";
|
|---|
| 609 | }
|
|---|
| 610 | if (layer.format == null) {
|
|---|
| 611 | // no format found - it's mandatory parameter - can't use this layer
|
|---|
| 612 | Logging.warn(tr("Can''t use layer {0} because no supported formats were found. Layer is available in formats: {1}",
|
|---|
| 613 | layer.getUserTitle(),
|
|---|
| 614 | String.join(", ", unsupportedFormats)));
|
|---|
| 615 | return null;
|
|---|
| 616 | }
|
|---|
| 617 | // Java has issues if spaces are not URL encoded. Ensure that we URL encode the spaces.
|
|---|
| 618 | if (layer.format.contains(" ")) {
|
|---|
| 619 | layer.format = layer.format.replace(" ", "%20");
|
|---|
| 620 | }
|
|---|
| 621 | return layer;
|
|---|
| 622 | }
|
|---|
| 623 |
|
|---|
| 624 | /**
|
|---|
| 625 | * Gets Dimension value. Returns when reader is on Dimension closing tag
|
|---|
| 626 | *
|
|---|
| 627 | * @param reader StAX reader instance
|
|---|
| 628 | * @return dimension
|
|---|
| 629 | * @throws XMLStreamException See {@link XMLStreamReader}
|
|---|
| 630 | */
|
|---|
| 631 | private static Dimension parseDimension(XMLStreamReader reader) throws XMLStreamException {
|
|---|
| 632 | Dimension ret = new Dimension();
|
|---|
| 633 | for (int event = reader.getEventType();
|
|---|
| 634 | reader.hasNext() && !(event == END_ELEMENT && QN_DIMENSION.equals(reader.getName()));
|
|---|
| 635 | event = reader.next()) {
|
|---|
| 636 | if (event == START_ELEMENT) {
|
|---|
| 637 | QName qName = reader.getName();
|
|---|
| 638 | if (QN_OWS_IDENTIFIER.equals(qName)) {
|
|---|
| 639 | ret.identifier = reader.getElementText();
|
|---|
| 640 | } else if (QN_DEFAULT.equals(qName)) {
|
|---|
| 641 | ret.defaultValue = reader.getElementText();
|
|---|
| 642 | } else if (QN_VALUE.equals(qName)) {
|
|---|
| 643 | ret.values.add(reader.getElementText());
|
|---|
| 644 | }
|
|---|
| 645 | }
|
|---|
| 646 | }
|
|---|
| 647 | return ret;
|
|---|
| 648 | }
|
|---|
| 649 |
|
|---|
| 650 | /**
|
|---|
| 651 | * Gets TileMatrixSetLink value. Returns when reader is on TileMatrixSetLink closing tag
|
|---|
| 652 | *
|
|---|
| 653 | * @param reader StAX reader instance
|
|---|
| 654 | * @return TileMatrixSetLink identifier
|
|---|
| 655 | * @throws XMLStreamException See {@link XMLStreamReader}
|
|---|
| 656 | */
|
|---|
| 657 | private static String parseTileMatrixSetLink(XMLStreamReader reader) throws XMLStreamException {
|
|---|
| 658 | String ret = null;
|
|---|
| 659 | for (int event = reader.getEventType();
|
|---|
| 660 | reader.hasNext() && !(event == END_ELEMENT && QN_TILEMATRIX_SET_LINK.equals(reader.getName()));
|
|---|
| 661 | event = reader.next()) {
|
|---|
| 662 | if (event == START_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName())) {
|
|---|
| 663 | ret = reader.getElementText();
|
|---|
| 664 | }
|
|---|
| 665 | }
|
|---|
| 666 | return ret;
|
|---|
| 667 | }
|
|---|
| 668 |
|
|---|
| 669 | /**
|
|---|
| 670 | * Parses TileMatrixSet section. Returns when reader is on TileMatrixSet closing tag
|
|---|
| 671 | * @param reader StAX reader instance
|
|---|
| 672 | * @return TileMatrixSet object
|
|---|
| 673 | * @throws XMLStreamException See {@link XMLStreamReader}
|
|---|
| 674 | */
|
|---|
| 675 | private static TileMatrixSet parseTileMatrixSet(XMLStreamReader reader) throws XMLStreamException {
|
|---|
| 676 | TileMatrixSetBuilder matrixSet = new TileMatrixSetBuilder();
|
|---|
| 677 | for (int event = reader.getEventType();
|
|---|
| 678 | reader.hasNext() && !(event == END_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName()));
|
|---|
| 679 | event = reader.next()) {
|
|---|
| 680 | if (event == START_ELEMENT) {
|
|---|
| 681 | QName qName = reader.getName();
|
|---|
| 682 | if (QN_OWS_IDENTIFIER.equals(qName)) {
|
|---|
| 683 | matrixSet.identifier = reader.getElementText();
|
|---|
| 684 | } else if (QN_OWS_SUPPORTED_CRS.equals(qName)) {
|
|---|
| 685 | matrixSet.crs = GetCapabilitiesParseHelper.crsToCode(reader.getElementText());
|
|---|
| 686 | } else if (QN_TILEMATRIX.equals(qName)) {
|
|---|
| 687 | matrixSet.tileMatrix.add(parseTileMatrix(reader, matrixSet.crs));
|
|---|
| 688 | }
|
|---|
| 689 | }
|
|---|
| 690 | }
|
|---|
| 691 | return matrixSet.build();
|
|---|
| 692 | }
|
|---|
| 693 |
|
|---|
| 694 | /**
|
|---|
| 695 | * Parses TileMatrix section. Returns when reader is on TileMatrix closing tag.
|
|---|
| 696 | * @param reader StAX reader instance
|
|---|
| 697 | * @param matrixCrs projection used by this matrix
|
|---|
| 698 | * @return TileMatrix object
|
|---|
| 699 | * @throws XMLStreamException See {@link XMLStreamReader}
|
|---|
| 700 | */
|
|---|
| 701 | private static TileMatrix parseTileMatrix(XMLStreamReader reader, String matrixCrs) throws XMLStreamException {
|
|---|
| 702 | Projection matrixProj = Optional.ofNullable(Projections.getProjectionByCode(matrixCrs))
|
|---|
| 703 | .orElseGet(ProjectionRegistry::getProjection); // use current projection if none found. Maybe user is using custom string
|
|---|
| 704 | TileMatrix ret = new TileMatrix();
|
|---|
| 705 | for (int event = reader.getEventType();
|
|---|
| 706 | reader.hasNext() && !(event == END_ELEMENT && QN_TILEMATRIX.equals(reader.getName()));
|
|---|
| 707 | event = reader.next()) {
|
|---|
| 708 | if (event == START_ELEMENT) {
|
|---|
| 709 | QName qName = reader.getName();
|
|---|
| 710 | if (QN_OWS_IDENTIFIER.equals(qName)) {
|
|---|
| 711 | ret.identifier = reader.getElementText();
|
|---|
| 712 | } else if (QN_SCALE_DENOMINATOR.equals(qName)) {
|
|---|
| 713 | ret.scaleDenominator = Double.parseDouble(reader.getElementText());
|
|---|
| 714 | } else if (QN_TOPLEFT_CORNER.equals(qName)) {
|
|---|
| 715 | ret.topLeftCorner = parseEastNorth(reader.getElementText(), matrixProj.switchXY());
|
|---|
| 716 | } else if (QN_TILE_HEIGHT.equals(qName)) {
|
|---|
| 717 | ret.tileHeight = Integer.parseInt(reader.getElementText());
|
|---|
| 718 | } else if (QN_TILE_WIDTH.equals(qName)) {
|
|---|
| 719 | ret.tileWidth = Integer.parseInt(reader.getElementText());
|
|---|
| 720 | } else if (QN_MATRIX_HEIGHT.equals(qName)) {
|
|---|
| 721 | ret.matrixHeight = Integer.parseInt(reader.getElementText());
|
|---|
| 722 | } else if (QN_MATRIX_WIDTH.equals(qName)) {
|
|---|
| 723 | ret.matrixWidth = Integer.parseInt(reader.getElementText());
|
|---|
| 724 | }
|
|---|
| 725 | }
|
|---|
| 726 | }
|
|---|
| 727 | if (ret.tileHeight != ret.tileWidth) {
|
|---|
| 728 | throw new AssertionError(tr("Only square tiles are supported. {0}x{1} returned by server for TileMatrix identifier {2}",
|
|---|
| 729 | ret.tileHeight, ret.tileWidth, ret.identifier));
|
|---|
| 730 | }
|
|---|
| 731 | return ret;
|
|---|
| 732 | }
|
|---|
| 733 |
|
|---|
| 734 | private static <T> T parseCoor(String coor, boolean switchXY, BiFunction<String, String, T> function) {
|
|---|
| 735 | String[] parts = coor.split(" ", -1);
|
|---|
| 736 | if (switchXY) {
|
|---|
| 737 | return function.apply(parts[1], parts[0]);
|
|---|
| 738 | } else {
|
|---|
| 739 | return function.apply(parts[0], parts[1]);
|
|---|
| 740 | }
|
|---|
| 741 | }
|
|---|
| 742 |
|
|---|
| 743 | private static EastNorth parseEastNorth(String coor, boolean switchXY) {
|
|---|
| 744 | return parseCoor(coor, switchXY, (e, n) -> new EastNorth(Double.parseDouble(e), Double.parseDouble(n)));
|
|---|
| 745 | }
|
|---|
| 746 |
|
|---|
| 747 | private static LatLon parseLatLon(String coor, boolean switchXY) {
|
|---|
| 748 | return parseCoor(coor, switchXY, (lon, lat) -> new LatLon(Double.parseDouble(lat), Double.parseDouble(lon)));
|
|---|
| 749 | }
|
|---|
| 750 |
|
|---|
| 751 | /**
|
|---|
| 752 | * Parses WGS84BoundingBox section. Returns when reader is on WGS84BoundingBox closing tag.
|
|---|
| 753 | * @param reader StAX reader instance
|
|---|
| 754 | * @return WGS84 bounding box
|
|---|
| 755 | * @throws XMLStreamException See {@link XMLStreamReader}
|
|---|
| 756 | */
|
|---|
| 757 | private static BBox parseBoundingBox(XMLStreamReader reader) throws XMLStreamException {
|
|---|
| 758 | LatLon lowerCorner = null;
|
|---|
| 759 | LatLon upperCorner = null;
|
|---|
| 760 | for (int event = reader.getEventType();
|
|---|
| 761 | reader.hasNext() && !(event == END_ELEMENT && QN_OWS_WGS84_BOUNDING_BOX.equals(reader.getName()));
|
|---|
| 762 | event = reader.next()) {
|
|---|
| 763 | if (event == START_ELEMENT) {
|
|---|
| 764 | QName qName = reader.getName();
|
|---|
| 765 | if (QN_OWS_LOWER_CORNER.equals(qName)) {
|
|---|
| 766 | lowerCorner = parseLatLon(reader.getElementText(), false);
|
|---|
| 767 | } else if (QN_OWS_UPPER_CORNER.equals(qName)) {
|
|---|
| 768 | upperCorner = parseLatLon(reader.getElementText(), false);
|
|---|
| 769 | }
|
|---|
| 770 | }
|
|---|
| 771 | }
|
|---|
| 772 | if (lowerCorner != null && upperCorner != null) {
|
|---|
| 773 | return new BBox(lowerCorner, upperCorner);
|
|---|
| 774 | }
|
|---|
| 775 | return null;
|
|---|
| 776 | }
|
|---|
| 777 |
|
|---|
| 778 | /**
|
|---|
| 779 | * Parses OperationMetadata section. Returns when reader is on OperationsMetadata closing tag.
|
|---|
| 780 | * return WMTSCapabilities with baseUrl and transferMode
|
|---|
| 781 | *
|
|---|
| 782 | * @param reader StAX reader instance
|
|---|
| 783 | * @return WMTSCapabilities with baseUrl and transferMode set
|
|---|
| 784 | * @throws XMLStreamException See {@link XMLStreamReader}
|
|---|
| 785 | */
|
|---|
| 786 | private static WMTSCapabilities parseOperationMetadata(XMLStreamReader reader) throws XMLStreamException {
|
|---|
| 787 | for (int event = reader.getEventType();
|
|---|
| 788 | reader.hasNext() && !(event == END_ELEMENT && QN_OWS_OPERATIONS_METADATA.equals(reader.getName()));
|
|---|
| 789 | event = reader.next()) {
|
|---|
| 790 | if (event == START_ELEMENT &&
|
|---|
| 791 | QN_OWS_OPERATION.equals(reader.getName()) &&
|
|---|
| 792 | "GetTile".equals(reader.getAttributeValue("", "name")) &&
|
|---|
| 793 | GetCapabilitiesParseHelper.moveReaderToTag(reader, QN_OWS_DCP, QN_OWS_HTTP, QN_OWS_GET)) {
|
|---|
| 794 | return new WMTSCapabilities(
|
|---|
| 795 | reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href"),
|
|---|
| 796 | GetCapabilitiesParseHelper.getTransferMode(reader)
|
|---|
| 797 | );
|
|---|
| 798 | }
|
|---|
| 799 | }
|
|---|
| 800 | return null;
|
|---|
| 801 | }
|
|---|
| 802 |
|
|---|
| 803 | /**
|
|---|
| 804 | * Initializes projection for this TileSource with projection
|
|---|
| 805 | * @param proj projection to be used by this TileSource
|
|---|
| 806 | */
|
|---|
| 807 | public void initProjection(Projection proj) {
|
|---|
| 808 | if (proj.equals(tileProjection))
|
|---|
| 809 | return;
|
|---|
| 810 | List<Layer> matchingLayers = layers.stream().filter(
|
|---|
| 811 | l -> l.identifier.equals(defaultLayer.getLayerName()) && l.tileMatrixSet.crs.equals(proj.toCode()))
|
|---|
| 812 | .collect(Collectors.toList());
|
|---|
| 813 | if (matchingLayers.size() > 1) {
|
|---|
| 814 | this.currentLayer = matchingLayers.stream().filter(
|
|---|
| 815 | l -> l.tileMatrixSet.identifier.equals(defaultLayer.getTileMatrixSet()))
|
|---|
| 816 | .findFirst().orElse(matchingLayers.get(0));
|
|---|
| 817 | this.tileProjection = proj;
|
|---|
| 818 | } else if (matchingLayers.size() == 1) {
|
|---|
| 819 | this.currentLayer = matchingLayers.get(0);
|
|---|
| 820 | this.tileProjection = proj;
|
|---|
| 821 | } else {
|
|---|
| 822 | // no tile matrix sets with current projection
|
|---|
| 823 | if (this.currentLayer == null) {
|
|---|
| 824 | this.tileProjection = null;
|
|---|
| 825 | for (Layer layer : layers) {
|
|---|
| 826 | if (!layer.identifier.equals(defaultLayer.getLayerName())) {
|
|---|
| 827 | continue;
|
|---|
| 828 | }
|
|---|
| 829 | Projection pr = Projections.getProjectionByCode(layer.tileMatrixSet.crs);
|
|---|
| 830 | if (pr != null) {
|
|---|
| 831 | this.currentLayer = layer;
|
|---|
| 832 | this.tileProjection = pr;
|
|---|
| 833 | break;
|
|---|
| 834 | }
|
|---|
| 835 | }
|
|---|
| 836 | if (this.currentLayer == null)
|
|---|
| 837 | throw new IllegalArgumentException(
|
|---|
| 838 | layers.stream().map(l -> l.tileMatrixSet).collect(Collectors.toList()).toString());
|
|---|
| 839 | } // else: keep currentLayer and tileProjection as is
|
|---|
| 840 | }
|
|---|
| 841 | if (this.currentLayer != null) {
|
|---|
| 842 | this.currentTileMatrixSet = this.currentLayer.tileMatrixSet;
|
|---|
| 843 | Collection<Double> scales = currentTileMatrixSet.tileMatrix.stream()
|
|---|
| 844 | .map(tileMatrix -> tileMatrix.scaleDenominator * 0.28e-03)
|
|---|
| 845 | .collect(Collectors.toList());
|
|---|
| 846 | this.nativeScaleList = new ScaleList(scales);
|
|---|
| 847 | }
|
|---|
| 848 | this.crsScale = getTileSize() * 0.28e-03 / this.tileProjection.getMetersPerUnit();
|
|---|
| 849 | }
|
|---|
| 850 |
|
|---|
| 851 | @Override
|
|---|
| 852 | public int getTileSize() {
|
|---|
| 853 | if (cachedTileSize > 0) {
|
|---|
| 854 | return cachedTileSize;
|
|---|
| 855 | }
|
|---|
| 856 | if (currentTileMatrixSet != null) {
|
|---|
| 857 | // no support for non-square tiles (tileHeight != tileWidth)
|
|---|
| 858 | // and for different tile sizes at different zoom levels
|
|---|
| 859 | cachedTileSize = currentTileMatrixSet.tileMatrix.get(0).tileHeight;
|
|---|
| 860 | return cachedTileSize;
|
|---|
| 861 | }
|
|---|
| 862 | // Fallback to default mercator tile size. Maybe it will work
|
|---|
| 863 | Logging.warn("WMTS: Could not determine tile size. Using default tile size of: {0}", getDefaultTileSize());
|
|---|
| 864 | return getDefaultTileSize();
|
|---|
| 865 | }
|
|---|
| 866 |
|
|---|
| 867 | @Override
|
|---|
| 868 | public String getTileUrl(int zoom, int tilex, int tiley) {
|
|---|
| 869 | if (currentLayer == null) {
|
|---|
| 870 | return "";
|
|---|
| 871 | }
|
|---|
| 872 |
|
|---|
| 873 | String url;
|
|---|
| 874 | if (currentLayer.baseUrl != null && transferMode == null) {
|
|---|
| 875 | url = currentLayer.baseUrl;
|
|---|
| 876 | } else {
|
|---|
| 877 | switch (transferMode) {
|
|---|
| 878 | case KVP:
|
|---|
| 879 | url = baseUrl + URL_GET_ENCODING_PARAMS;
|
|---|
| 880 | break;
|
|---|
| 881 | case REST:
|
|---|
| 882 | url = currentLayer.baseUrl;
|
|---|
| 883 | break;
|
|---|
| 884 | default:
|
|---|
| 885 | url = "";
|
|---|
| 886 | break;
|
|---|
| 887 | }
|
|---|
| 888 | }
|
|---|
| 889 |
|
|---|
| 890 | TileMatrix tileMatrix = getTileMatrix(zoom);
|
|---|
| 891 |
|
|---|
| 892 | if (tileMatrix == null) {
|
|---|
| 893 | return ""; // no matrix, probably unsupported CRS selected.
|
|---|
| 894 | }
|
|---|
| 895 |
|
|---|
| 896 | url = url.replace("{layer}", this.currentLayer.identifier)
|
|---|
| 897 | .replace("{format}", this.currentLayer.format)
|
|---|
| 898 | .replace("{TileMatrixSet}", this.currentTileMatrixSet.identifier)
|
|---|
| 899 | .replace("{TileMatrix}", tileMatrix.identifier)
|
|---|
| 900 | .replace("{TileRow}", Integer.toString(tiley))
|
|---|
| 901 | .replace("{TileCol}", Integer.toString(tilex))
|
|---|
| 902 | .replaceAll("(?i)\\{style}", this.currentLayer.style);
|
|---|
| 903 |
|
|---|
| 904 | for (Dimension d : currentLayer.dimensions) {
|
|---|
| 905 | url = url.replaceAll("(?i)\\{"+d.identifier+"}", d.defaultValue);
|
|---|
| 906 | }
|
|---|
| 907 |
|
|---|
| 908 | return url;
|
|---|
| 909 | }
|
|---|
| 910 |
|
|---|
| 911 | /**
|
|---|
| 912 | * Returns TileMatrix that's working on given zoom level
|
|---|
| 913 | * @param zoom zoom level
|
|---|
| 914 | * @return TileMatrix that's working on this zoom level
|
|---|
| 915 | */
|
|---|
| 916 | private TileMatrix getTileMatrix(int zoom) {
|
|---|
| 917 | if (zoom > getMaxZoom()) {
|
|---|
| 918 | return null;
|
|---|
| 919 | }
|
|---|
| 920 | if (zoom < 0) {
|
|---|
| 921 | return null;
|
|---|
| 922 | }
|
|---|
| 923 | return this.currentTileMatrixSet.tileMatrix.get(zoom);
|
|---|
| 924 | }
|
|---|
| 925 |
|
|---|
| 926 | @Override
|
|---|
| 927 | public double getDistance(double lat1, double lon1, double lat2, double lon2) {
|
|---|
| 928 | throw new UnsupportedOperationException("Not implemented");
|
|---|
| 929 | }
|
|---|
| 930 |
|
|---|
| 931 | @Override
|
|---|
| 932 | public ICoordinate tileXYToLatLon(int x, int y, int zoom) {
|
|---|
| 933 | TileMatrix matrix = getTileMatrix(zoom);
|
|---|
| 934 | if (matrix == null) {
|
|---|
| 935 | return CoordinateConversion.llToCoor(tileProjection.getWorldBoundsLatLon().getCenter());
|
|---|
| 936 | }
|
|---|
| 937 | double scale = matrix.scaleDenominator * this.crsScale;
|
|---|
| 938 | EastNorth ret = new EastNorth(matrix.topLeftCorner.east() + x * scale, matrix.topLeftCorner.north() - y * scale);
|
|---|
| 939 | return CoordinateConversion.llToCoor(tileProjection.eastNorth2latlon(ret));
|
|---|
| 940 | }
|
|---|
| 941 |
|
|---|
| 942 | @Override
|
|---|
| 943 | public TileXY latLonToTileXY(double lat, double lon, int zoom) {
|
|---|
| 944 | TileMatrix matrix = getTileMatrix(zoom);
|
|---|
| 945 | if (matrix == null) {
|
|---|
| 946 | return new TileXY(0, 0);
|
|---|
| 947 | }
|
|---|
| 948 |
|
|---|
| 949 | EastNorth enPoint = tileProjection.latlon2eastNorth(new LatLon(lat, lon));
|
|---|
| 950 | double scale = matrix.scaleDenominator * this.crsScale;
|
|---|
| 951 | return new TileXY(
|
|---|
| 952 | (enPoint.east() - matrix.topLeftCorner.east()) / scale,
|
|---|
| 953 | (matrix.topLeftCorner.north() - enPoint.north()) / scale
|
|---|
| 954 | );
|
|---|
| 955 | }
|
|---|
| 956 |
|
|---|
| 957 | @Override
|
|---|
| 958 | public int getTileXMax(int zoom) {
|
|---|
| 959 | return getTileXMax(zoom, tileProjection);
|
|---|
| 960 | }
|
|---|
| 961 |
|
|---|
| 962 | @Override
|
|---|
| 963 | public int getTileYMax(int zoom) {
|
|---|
| 964 | return getTileYMax(zoom, tileProjection);
|
|---|
| 965 | }
|
|---|
| 966 |
|
|---|
| 967 | @Override
|
|---|
| 968 | public Point latLonToXY(double lat, double lon, int zoom) {
|
|---|
| 969 | TileMatrix matrix = getTileMatrix(zoom);
|
|---|
| 970 | if (matrix == null) {
|
|---|
| 971 | return new Point(0, 0);
|
|---|
| 972 | }
|
|---|
| 973 | double scale = matrix.scaleDenominator * this.crsScale;
|
|---|
| 974 | EastNorth point = tileProjection.latlon2eastNorth(new LatLon(lat, lon));
|
|---|
| 975 | return new Point(
|
|---|
| 976 | (int) Math.round((point.east() - matrix.topLeftCorner.east()) / scale),
|
|---|
| 977 | (int) Math.round((matrix.topLeftCorner.north() - point.north()) / scale)
|
|---|
| 978 | );
|
|---|
| 979 | }
|
|---|
| 980 |
|
|---|
| 981 | @Override
|
|---|
| 982 | public Coordinate xyToLatLon(int x, int y, int zoom) {
|
|---|
| 983 | TileMatrix matrix = getTileMatrix(zoom);
|
|---|
| 984 | if (matrix == null) {
|
|---|
| 985 | return new Coordinate(0, 0);
|
|---|
| 986 | }
|
|---|
| 987 | double scale = matrix.scaleDenominator * this.crsScale;
|
|---|
| 988 | EastNorth ret = new EastNorth(
|
|---|
| 989 | matrix.topLeftCorner.east() + x * scale,
|
|---|
| 990 | matrix.topLeftCorner.north() - y * scale
|
|---|
| 991 | );
|
|---|
| 992 | LatLon ll = tileProjection.eastNorth2latlon(ret);
|
|---|
| 993 | return new Coordinate(ll.lat(), ll.lon());
|
|---|
| 994 | }
|
|---|
| 995 |
|
|---|
| 996 | @Override
|
|---|
| 997 | public Map<String, String> getHeaders() {
|
|---|
| 998 | return headers;
|
|---|
| 999 | }
|
|---|
| 1000 |
|
|---|
| 1001 | @Override
|
|---|
| 1002 | public int getMaxZoom() {
|
|---|
| 1003 | if (this.currentTileMatrixSet != null) {
|
|---|
| 1004 | return this.currentTileMatrixSet.getMaxZoom();
|
|---|
| 1005 | }
|
|---|
| 1006 | return 0;
|
|---|
| 1007 | }
|
|---|
| 1008 |
|
|---|
| 1009 | @Override
|
|---|
| 1010 | public String getTileId(int zoom, int tilex, int tiley) {
|
|---|
| 1011 | return getTileUrl(zoom, tilex, tiley);
|
|---|
| 1012 | }
|
|---|
| 1013 |
|
|---|
| 1014 | /**
|
|---|
| 1015 | * Checks if url is acceptable by this Tile Source
|
|---|
| 1016 | * @param url URL to check
|
|---|
| 1017 | */
|
|---|
| 1018 | public static void checkUrl(String url) {
|
|---|
| 1019 | ImageryPatterns.checkWmtsUrlPatterns(url);
|
|---|
| 1020 | }
|
|---|
| 1021 |
|
|---|
| 1022 | /**
|
|---|
| 1023 | * Group layers by name and tile matrix set.
|
|---|
| 1024 | * @param layers to be grouped
|
|---|
| 1025 | * @return list with entries - grouping identifier + list of layers
|
|---|
| 1026 | */
|
|---|
| 1027 | public static List<Entry<String, List<Layer>>> groupLayersByNameAndTileMatrixSet(Collection<Layer> layers) {
|
|---|
| 1028 | Map<String, List<Layer>> layerByName = layers.stream().collect(
|
|---|
| 1029 | Collectors.groupingBy(x -> x.identifier + '\u001c' + x.tileMatrixSet.identifier));
|
|---|
| 1030 | return layerByName.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList());
|
|---|
| 1031 | }
|
|---|
| 1032 |
|
|---|
| 1033 | /**
|
|---|
| 1034 | * Returns set of projection codes that this TileSource supports.
|
|---|
| 1035 | * @return set of projection codes that this TileSource supports
|
|---|
| 1036 | */
|
|---|
| 1037 | public Collection<String> getSupportedProjections() {
|
|---|
| 1038 | return this.layers.stream()
|
|---|
| 1039 | .filter(layer -> currentLayer == null || currentLayer.identifier.equals(layer.identifier))
|
|---|
| 1040 | .map(layer -> layer.tileMatrixSet.crs)
|
|---|
| 1041 | .collect(Collectors.toCollection(LinkedHashSet::new));
|
|---|
| 1042 | }
|
|---|
| 1043 |
|
|---|
| 1044 | private int getTileYMax(int zoom, Projection proj) {
|
|---|
| 1045 | TileMatrix matrix = getTileMatrix(zoom);
|
|---|
| 1046 | if (matrix == null) {
|
|---|
| 1047 | return 0;
|
|---|
| 1048 | }
|
|---|
| 1049 |
|
|---|
| 1050 | if (matrix.matrixHeight != -1) {
|
|---|
| 1051 | return matrix.matrixHeight;
|
|---|
| 1052 | }
|
|---|
| 1053 |
|
|---|
| 1054 | double scale = matrix.scaleDenominator * this.crsScale;
|
|---|
| 1055 | EastNorth min = matrix.topLeftCorner;
|
|---|
| 1056 | EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax());
|
|---|
| 1057 | return (int) Math.ceil(Math.abs(max.north() - min.north()) / scale);
|
|---|
| 1058 | }
|
|---|
| 1059 |
|
|---|
| 1060 | private int getTileXMax(int zoom, Projection proj) {
|
|---|
| 1061 | TileMatrix matrix = getTileMatrix(zoom);
|
|---|
| 1062 | if (matrix == null) {
|
|---|
| 1063 | return 0;
|
|---|
| 1064 | }
|
|---|
| 1065 | if (matrix.matrixWidth != -1) {
|
|---|
| 1066 | return matrix.matrixWidth;
|
|---|
| 1067 | }
|
|---|
| 1068 |
|
|---|
| 1069 | double scale = matrix.scaleDenominator * this.crsScale;
|
|---|
| 1070 | EastNorth min = matrix.topLeftCorner;
|
|---|
| 1071 | EastNorth max = proj.latlon2eastNorth(proj.getWorldBoundsLatLon().getMax());
|
|---|
| 1072 | return (int) Math.ceil(Math.abs(max.east() - min.east()) / scale);
|
|---|
| 1073 | }
|
|---|
| 1074 |
|
|---|
| 1075 | /**
|
|---|
| 1076 | * Get native scales of tile source.
|
|---|
| 1077 | * @return {@link ScaleList} of native scales
|
|---|
| 1078 | */
|
|---|
| 1079 | public ScaleList getNativeScales() {
|
|---|
| 1080 | return nativeScaleList;
|
|---|
| 1081 | }
|
|---|
| 1082 |
|
|---|
| 1083 | /**
|
|---|
| 1084 | * Returns the tile projection.
|
|---|
| 1085 | * @return the tile projection
|
|---|
| 1086 | */
|
|---|
| 1087 | public Projection getTileProjection() {
|
|---|
| 1088 | return tileProjection;
|
|---|
| 1089 | }
|
|---|
| 1090 |
|
|---|
| 1091 | @Override
|
|---|
| 1092 | public IProjected tileXYtoProjected(int x, int y, int zoom) {
|
|---|
| 1093 | TileMatrix matrix = getTileMatrix(zoom);
|
|---|
| 1094 | if (matrix == null) {
|
|---|
| 1095 | return new Projected(0, 0);
|
|---|
| 1096 | }
|
|---|
| 1097 | double scale = matrix.scaleDenominator * this.crsScale;
|
|---|
| 1098 | return new Projected(
|
|---|
| 1099 | matrix.topLeftCorner.east() + x * scale,
|
|---|
| 1100 | matrix.topLeftCorner.north() - y * scale);
|
|---|
| 1101 | }
|
|---|
| 1102 |
|
|---|
| 1103 | @Override
|
|---|
| 1104 | public TileXY projectedToTileXY(IProjected projected, int zoom) {
|
|---|
| 1105 | TileMatrix matrix = getTileMatrix(zoom);
|
|---|
| 1106 | if (matrix == null) {
|
|---|
| 1107 | return new TileXY(0, 0);
|
|---|
| 1108 | }
|
|---|
| 1109 | double scale = matrix.scaleDenominator * this.crsScale;
|
|---|
| 1110 | return new TileXY(
|
|---|
| 1111 | (projected.getEast() - matrix.topLeftCorner.east()) / scale,
|
|---|
| 1112 | -(projected.getNorth() - matrix.topLeftCorner.north()) / scale);
|
|---|
| 1113 | }
|
|---|
| 1114 |
|
|---|
| 1115 | private EastNorth tileToEastNorth(int x, int y, int z) {
|
|---|
| 1116 | return CoordinateConversion.projToEn(this.tileXYtoProjected(x, y, z));
|
|---|
| 1117 | }
|
|---|
| 1118 |
|
|---|
| 1119 | private ProjectionBounds getTileProjectionBounds(Tile tile) {
|
|---|
| 1120 | ProjectionBounds pb = new ProjectionBounds(tileToEastNorth(tile.getXtile(), tile.getYtile(), tile.getZoom()));
|
|---|
| 1121 | pb.extend(tileToEastNorth(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom()));
|
|---|
| 1122 | return pb;
|
|---|
| 1123 | }
|
|---|
| 1124 |
|
|---|
| 1125 | @Override
|
|---|
| 1126 | public boolean isInside(Tile inner, Tile outer) {
|
|---|
| 1127 | ProjectionBounds pbInner = getTileProjectionBounds(inner);
|
|---|
| 1128 | ProjectionBounds pbOuter = getTileProjectionBounds(outer);
|
|---|
| 1129 | // a little tolerance, for when inner tile touches the border of the outer tile
|
|---|
| 1130 | double epsilon = 1e-7 * (pbOuter.maxEast - pbOuter.minEast);
|
|---|
| 1131 | return pbOuter.minEast <= pbInner.minEast + epsilon &&
|
|---|
| 1132 | pbOuter.minNorth <= pbInner.minNorth + epsilon &&
|
|---|
| 1133 | pbOuter.maxEast >= pbInner.maxEast - epsilon &&
|
|---|
| 1134 | pbOuter.maxNorth >= pbInner.maxNorth - epsilon;
|
|---|
| 1135 | }
|
|---|
| 1136 |
|
|---|
| 1137 | @Override
|
|---|
| 1138 | public TileRange getCoveringTileRange(Tile tile, int newZoom) {
|
|---|
| 1139 | TileMatrix matrixNew = getTileMatrix(newZoom);
|
|---|
| 1140 | if (matrixNew == null) {
|
|---|
| 1141 | return new TileRange(new TileXY(0, 0), new TileXY(0, 0), newZoom);
|
|---|
| 1142 | }
|
|---|
| 1143 | IProjected p0 = tileXYtoProjected(tile.getXtile(), tile.getYtile(), tile.getZoom());
|
|---|
| 1144 | IProjected p1 = tileXYtoProjected(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
|
|---|
| 1145 | TileXY tMin = projectedToTileXY(p0, newZoom);
|
|---|
| 1146 | TileXY tMax = projectedToTileXY(p1, newZoom);
|
|---|
| 1147 | // shrink the target tile a little, so we don't get neighboring tiles, that
|
|---|
| 1148 | // share an edge, but don't actually cover the target tile
|
|---|
| 1149 | double epsilon = 1e-7 * (tMax.getX() - tMin.getX());
|
|---|
| 1150 | int minX = (int) Math.floor(tMin.getX() + epsilon);
|
|---|
| 1151 | int minY = (int) Math.floor(tMin.getY() + epsilon);
|
|---|
| 1152 | int maxX = (int) Math.ceil(tMax.getX() - epsilon) - 1;
|
|---|
| 1153 | int maxY = (int) Math.ceil(tMax.getY() - epsilon) - 1;
|
|---|
| 1154 | return new TileRange(new TileXY(minX, minY), new TileXY(maxX, maxY), newZoom);
|
|---|
| 1155 | }
|
|---|
| 1156 |
|
|---|
| 1157 | @Override
|
|---|
| 1158 | public String getServerCRS() {
|
|---|
| 1159 | return tileProjection != null ? tileProjection.toCode() : null;
|
|---|
| 1160 | }
|
|---|
| 1161 |
|
|---|
| 1162 | /**
|
|---|
| 1163 | * Layers that can be used with this tile source
|
|---|
| 1164 | * @return unmodifiable collection of layers available in this tile source
|
|---|
| 1165 | * @since 13879
|
|---|
| 1166 | */
|
|---|
| 1167 | public Collection<Layer> getLayers() {
|
|---|
| 1168 | return Collections.unmodifiableCollection(layers);
|
|---|
| 1169 | }
|
|---|
| 1170 | }
|
|---|