From a116878a57ac1d24ce852e5bdce81efe10d969fd Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Tue, 28 Apr 2015 23:14:08 +0200 Subject: [PATCH] Allow extents to restrict tile ranges requested from the server The addition of full extent tile ranges also allows us to simplify wrapX handling for tile layers. By limiting wrapX to true and false as possible values, we can remove a lot of guessing logic. --- examples/wms-custom-tilegrid-512x256.js | 3 +- examples/wms-tiled-wrap-180.js | 3 +- externs/olx.js | 92 +++++++--- src/ol/attribution.js | 4 +- src/ol/source/source.js | 4 +- src/ol/source/tileimagesource.js | 3 +- src/ol/source/tilesource.js | 24 ++- src/ol/source/tilewmssource.js | 2 +- src/ol/source/wmtssource.js | 42 ++--- src/ol/tilecoord.js | 41 +++-- src/ol/tilegrid/tilegrid.js | 153 ++++++++++------ src/ol/tilegrid/wmtstilegrid.js | 27 +-- test/spec/ol/source/tilesource.test.js | 29 +-- test/spec/ol/source/xyzsource.test.js | 21 ++- test/spec/ol/tilecoord.test.js | 72 ++++++++ test/spec/ol/tilegrid/tilegrid.test.js | 198 ++++++++++++++++++++- test/spec/ol/tilegrid/wmtstilegrid.test.js | 16 +- 17 files changed, 535 insertions(+), 199 deletions(-) diff --git a/examples/wms-custom-tilegrid-512x256.js b/examples/wms-custom-tilegrid-512x256.js index 8a6226d030..5c7d86644a 100644 --- a/examples/wms-custom-tilegrid-512x256.js +++ b/examples/wms-custom-tilegrid-512x256.js @@ -15,7 +15,7 @@ for (var i = 0, ii = resolutions.length; i < ii; ++i) { resolutions[i] = startResolution / Math.pow(2, i); } var tileGrid = new ol.tilegrid.TileGrid({ - origin: ol.extent.getBottomLeft(projExtent), + extent: [-13884991, 2870341, -7455066, 6338219], resolutions: resolutions, tileSize: [512, 256] }); @@ -25,7 +25,6 @@ var layers = [ source: new ol.source.MapQuest({layer: 'sat'}) }), new ol.layer.Tile({ - extent: [-13884991, 2870341, -7455066, 6338219], source: new ol.source.TileWMS({ url: 'http://demo.boundlessgeo.com/geoserver/wms', params: {'LAYERS': 'topp:states', 'TILED': true}, diff --git a/examples/wms-tiled-wrap-180.js b/examples/wms-tiled-wrap-180.js index fdf864b7e8..bf606e013f 100644 --- a/examples/wms-tiled-wrap-180.js +++ b/examples/wms-tiled-wrap-180.js @@ -13,8 +13,7 @@ var layers = [ source: new ol.source.TileWMS({ url: 'http://demo.boundlessgeo.com/geoserver/ne/wms', params: {'LAYERS': 'ne:ne_10m_admin_0_countries', 'TILED': true}, - serverType: 'geoserver', - wrapX: true + serverType: 'geoserver' }) }) ]; diff --git a/externs/olx.js b/externs/olx.js index f258393c28..ed472c0581 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -4942,11 +4942,10 @@ olx.source.TileWMSOptions.prototype.urls; /** - * Whether to wrap the world horizontally. The default, `undefined`, is to - * request out-of-bounds tiles from the server. This works well in e.g. - * GeoServer. When set to `false`, only one world will be rendered. When set to - * `true`, tiles will be requested for one world only, but they will be wrapped - * horizontally to render multiple worlds. + * Whether to wrap the world horizontally. When set to `false`, only one world + * will be rendered. When `true`, tiles will be requested for one world only, + * but they will be wrapped horizontally to render multiple worlds. The default + * is `true`. * @type {boolean|undefined} * @api */ @@ -6014,18 +6013,30 @@ olx.tilegrid; /** - * @typedef {{minZoom: (number|undefined), + * @typedef {{extent: (ol.Extent|undefined), + * minZoom: (number|undefined), * origin: (ol.Coordinate|undefined), * origins: (Array.|undefined), * resolutions: !Array., + * sizes: (Array.|undefined), * tileSize: (number|ol.Size|undefined), - * tileSizes: (Array.|undefined), - * widths: (Array.|undefined)}} + * tileSizes: (Array.|undefined)}} * @api */ olx.tilegrid.TileGridOptions; +/** + * Extent for the tile grid. No tiles outside this extent will be requested by + * {@link ol.source.Tile} sources. When no `origin` or `origins` are + * configured, the `origin` will be set to the bottom-left corner of the extent. + * When no `sizes` are configured, they will be calculated from the extent. + * @type {ol.Extent|undefined} + * @api + */ +olx.tilegrid.TileGridOptions.prototype.extent; + + /** * Minimum zoom. Default is 0. * @type {number|undefined} @@ -6035,7 +6046,7 @@ olx.tilegrid.TileGridOptions.prototype.minZoom; /** - * Origin. Default is null. + * Origin, i.e. the bottom-left corner of the grid. Default is null. * @type {ol.Coordinate|undefined} * @api stable */ @@ -6043,8 +6054,9 @@ olx.tilegrid.TileGridOptions.prototype.origin; /** - * Origins. If given, the array length should match the length of the - * `resolutions` array, i.e. each resolution can have a different origin. + * Origins, i.e. the bottom-left corners of the grid for each zoom level. If + * given, the array length should match the length of the `resolutions` array, + * i.e. each resolution can have a different origin. * @type {Array.|undefined} * @api stable */ @@ -6061,6 +6073,17 @@ olx.tilegrid.TileGridOptions.prototype.origins; olx.tilegrid.TileGridOptions.prototype.resolutions; +/** + * Number of tile rows and columns of the grid for each zoom level. This setting + * is only needed for tile coordinate transforms that need to work with origins + * other than the bottom-left corner of the grid. No tiles outside this range + * will be requested by sources. If an `extent` is also configured, it takes + * precedence. + * @type {Array.|undefined} + */ +olx.tilegrid.TileGridOptions.prototype.sizes; + + /** * Tile size. Default is `[256, 256]`. * @type {number|ol.Size|undefined} @@ -6079,32 +6102,32 @@ olx.tilegrid.TileGridOptions.prototype.tileSizes; /** - * Number of tile columns that cover the grid's extent for each zoom level. Only - * required when used with a source that has `wrapX` set to `true`, and only - * when the grid's origin differs from the one of the projection's extent. The - * array length has to match the length of the `resolutions` array, i.e. each - * resolution will have a matching entry here. - * @type {Array.|undefined} - * @api - */ -olx.tilegrid.TileGridOptions.prototype.widths; - - -/** - * @typedef {{origin: (ol.Coordinate|undefined), + * @typedef {{extent: (ol.Extent|undefined), + * origin: (ol.Coordinate|undefined), * origins: (Array.|undefined), * resolutions: !Array., * matrixIds: !Array., + * sizes: (Array.|undefined), * tileSize: (number|ol.Size|undefined), - * tileSizes: (Array.|undefined), - * widths: (Array.|undefined)}} + * tileSizes: (Array.|undefined)}} * @api */ olx.tilegrid.WMTSOptions; /** - * Origin. + * Extent for the tile grid. No tiles outside this extent will be requested by + * {@link ol.source.WMTS} sources. When no `origin` or `origins` are + * configured, the `origin` will be calculated from the extent. + * When no `sizes` are configured, they will be calculated from the extent. + * @type {ol.Extent|undefined} + * @api + */ +olx.tilegrid.WMTSOptions.prototype.extent; + + +/** + * Origin, i.e. the top-left corner of the grid. * @type {ol.Coordinate|undefined} * @api */ @@ -6112,7 +6135,8 @@ olx.tilegrid.WMTSOptions.prototype.origin; /** - * Origins. The length of this array needs to match the length of the + * Origins, i.e. the top-left corners of the grid for each zoom level. The + * length of this array needs to match the length of the * `resolutions` array. * @type {Array.|undefined} * @api @@ -6139,6 +6163,18 @@ olx.tilegrid.WMTSOptions.prototype.resolutions; olx.tilegrid.WMTSOptions.prototype.matrixIds; +/** + * Number of tile rows and columns of the grid for each zoom level. The values + * here are the `TileMatrixWidth` and `TileMatrixHeight` advertised in the + * GetCapabilities response of the WMTS, and define the grid's extent together + * with the `origin`. An `extent` can be configured in addition, and will + * further limit the extent for which tile requests are made by sources. + * @type {Array.|undefined} + * @api + */ +olx.tilegrid.WMTSOptions.prototype.sizes; + + /** * Tile size. * @type {number|ol.Size|undefined} diff --git a/src/ol/attribution.js b/src/ol/attribution.js index 4963fa08d6..9f069828c6 100644 --- a/src/ol/attribution.js +++ b/src/ol/attribution.js @@ -78,8 +78,8 @@ ol.Attribution.prototype.intersectsAnyTileRange = if (testTileRange.intersects(tileRange)) { return true; } - var extentTileRange = tileGrid.getTileRange( - parseInt(zKey, 10), projection); + var extentTileRange = tileGrid.getTileRangeForExtentAndZ( + projection.getExtent(), parseInt(zKey, 10)); var width = extentTileRange.getWidth(); if (tileRange.minX < extentTileRange.minX || tileRange.maxX > extentTileRange.maxX) { diff --git a/src/ol/source/source.js b/src/ol/source/source.js index e215cb40e9..a6aeab177c 100644 --- a/src/ol/source/source.js +++ b/src/ol/source/source.js @@ -75,9 +75,9 @@ ol.source.Source = function(options) { /** * @private - * @type {boolean|undefined} + * @type {boolean} */ - this.wrapX_ = options.wrapX; + this.wrapX_ = goog.isDef(options.wrapX) ? options.wrapX : false; }; goog.inherits(ol.source.Source, ol.Object); diff --git a/src/ol/source/tileimagesource.js b/src/ol/source/tileimagesource.js index 0b549a1f9c..1701f19e86 100644 --- a/src/ol/source/tileimagesource.js +++ b/src/ol/source/tileimagesource.js @@ -93,7 +93,8 @@ ol.source.TileImage.prototype.getTile = } else { goog.asserts.assert(projection, 'argument projection is truthy'); var tileCoord = [z, x, y]; - var urlTileCoord = this.getWrapXTileCoord(tileCoord, projection); + var urlTileCoord = this.getTileCoordForTileUrlFunction( + tileCoord, projection); var tileUrl = goog.isNull(urlTileCoord) ? undefined : this.tileUrlFunction(urlTileCoord, pixelRatio, projection); var tile = new this.tileClass( diff --git a/src/ol/source/tilesource.js b/src/ol/source/tilesource.js index e81a4261b3..a0179d5ad6 100644 --- a/src/ol/source/tilesource.js +++ b/src/ol/source/tilesource.js @@ -2,6 +2,7 @@ goog.provide('ol.source.Tile'); goog.provide('ol.source.TileEvent'); goog.provide('ol.source.TileOptions'); +goog.require('goog.asserts'); goog.require('goog.events.Event'); goog.require('ol.Attribution'); goog.require('ol.Extent'); @@ -216,28 +217,23 @@ ol.source.Tile.prototype.getTilePixelSize = /** - * Handles x-axis wrapping. When `wrapX` is `undefined` or the projection is not - * a global projection, `tileCoord` will be returned unaltered. When `wrapX` is - * `true`, the tile coordinate will be wrapped horizontally. - * When `wrapX` is `false`, `null` will be returned for tiles that are - * outside the projection extent. + * Handles x-axis wrapping and returns a tile coordinate when it is within + * the resolution and extent range. * @param {ol.TileCoord} tileCoord Tile coordinate. * @param {ol.proj.Projection=} opt_projection Projection. - * @return {ol.TileCoord} Tile coordinate. + * @return {ol.TileCoord} Tile coordinate to be passed to the tileUrlFunction or + * null if no tile URL should be created for the passed `tileCoord`. */ -ol.source.Tile.prototype.getWrapXTileCoord = +ol.source.Tile.prototype.getTileCoordForTileUrlFunction = function(tileCoord, opt_projection) { var projection = goog.isDef(opt_projection) ? opt_projection : this.getProjection(); var tileGrid = this.getTileGridForProjection(projection); - var wrapX = this.getWrapX(); - if (goog.isDef(wrapX) && tileGrid.isGlobal(tileCoord[0], projection)) { - return wrapX ? - ol.tilecoord.wrapX(tileCoord, tileGrid, projection) : - ol.tilecoord.clipX(tileCoord, tileGrid, projection); - } else { - return tileCoord; + goog.asserts.assert(!goog.isNull(tileGrid), 'tile grid needed'); + if (this.getWrapX()) { + tileCoord = ol.tilecoord.wrapX(tileCoord, tileGrid, projection); } + return ol.tilecoord.restrictByExtentAndZ(tileCoord, tileGrid); }; diff --git a/src/ol/source/tilewmssource.js b/src/ol/source/tilewmssource.js index d655a2e179..b2fa897e0a 100644 --- a/src/ol/source/tilewmssource.js +++ b/src/ol/source/tilewmssource.js @@ -49,7 +49,7 @@ ol.source.TileWMS = function(opt_options) { tileGrid: options.tileGrid, tileLoadFunction: options.tileLoadFunction, tileUrlFunction: goog.bind(this.tileUrlFunction_, this), - wrapX: options.wrapX + wrapX: goog.isDef(options.wrapX) ? options.wrapX : true }); var urls = options.urls; diff --git a/src/ol/source/wmtssource.js b/src/ol/source/wmtssource.js index 53eb1f3bcb..7eb25cdb2b 100644 --- a/src/ol/source/wmtssource.js +++ b/src/ol/source/wmtssource.js @@ -11,7 +11,6 @@ goog.require('ol.TileUrlFunctionType'); goog.require('ol.extent'); goog.require('ol.proj'); goog.require('ol.source.TileImage'); -goog.require('ol.tilecoord'); goog.require('ol.tilegrid.WMTS'); @@ -181,31 +180,8 @@ ol.source.WMTS = function(options) { goog.array.map(this.urls_, createFromWMTSTemplate)) : ol.TileUrlFunction.nullTileUrlFunction; - var tmpExtent = ol.extent.createEmpty(); tileUrlFunction = ol.TileUrlFunction.withTileCoordTransform( - /** - * @param {ol.TileCoord} tileCoord Tile coordinate. - * @param {ol.proj.Projection} projection Projection. - * @param {ol.TileCoord=} opt_tileCoord Tile coordinate. - * @return {ol.TileCoord} Tile coordinate. - */ - function(tileCoord, projection, opt_tileCoord) { - goog.asserts.assert(!goog.isNull(tileGrid), - 'tileGrid must not be null'); - if (tileGrid.getResolutions().length <= tileCoord[0]) { - return null; - } - var x = tileCoord[1]; - var y = -tileCoord[2] - 1; - var tileExtent = tileGrid.getTileCoordExtent(tileCoord, tmpExtent); - var extent = projection.getExtent(); - - if (!ol.extent.intersects(tileExtent, extent) || - ol.extent.touches(tileExtent, extent)) { - return null; - } - return ol.tilecoord.createOrUpdate(tileCoord[0], x, y, opt_tileCoord); - }, + ol.tilegrid.createOriginTopLeftTileCoordTransform(tileGrid), tileUrlFunction); goog.base(this, { @@ -450,9 +426,6 @@ ol.source.WMTS.optionsFromCapabilities = function(wmtsCap, config) { goog.asserts.assert(!goog.isNull(matrixSetObj), 'found matrixSet in Contents/TileMatrixSet'); - var tileGrid = ol.tilegrid.WMTS.createFromCapabilitiesMatrixSet( - matrixSetObj); - var projection; if (goog.isDef(config['projection'])) { projection = ol.proj.get(config['projection']); @@ -461,6 +434,19 @@ ol.source.WMTS.optionsFromCapabilities = function(wmtsCap, config) { /urn:ogc:def:crs:(\w+):(.*:)?(\w+)$/, '$1:$3')); } + var projectionExtent = projection.getExtent(); + var extent; + if (!goog.isNull(projectionExtent)) { + var projectionExtentWgs84 = ol.proj.transformExtent( + projectionExtent, projection, 'EPSG:4326'); + if (ol.extent.containsExtent(projectionExtentWgs84, wgs84BoundingBox)) { + extent = ol.proj.transformExtent( + wgs84BoundingBox, 'EPSG:4326', projection); + } + } + var tileGrid = ol.tilegrid.WMTS.createFromCapabilitiesMatrixSet( + matrixSetObj, extent); + /** @type {!Array.} */ var urls = []; var requestEncoding = config['requestEncoding']; diff --git a/src/ol/tilecoord.js b/src/ol/tilecoord.js index 09f7da088f..ebc1044add 100644 --- a/src/ol/tilecoord.js +++ b/src/ol/tilecoord.js @@ -3,7 +3,7 @@ goog.provide('ol.tilecoord'); goog.require('goog.array'); goog.require('goog.asserts'); -goog.require('goog.math'); +goog.require('ol.extent'); /** @@ -149,25 +149,42 @@ ol.tilecoord.toString = function(tileCoord) { */ ol.tilecoord.wrapX = function(tileCoord, tileGrid, projection) { var z = tileCoord[0]; - var x = tileCoord[1]; - var tileRange = tileGrid.getTileRange(z, projection); - if (x < tileRange.minX || x > tileRange.maxX) { - x = goog.math.modulo(x, tileRange.getWidth()); - return [z, x, tileCoord[2]]; + var center = tileGrid.getTileCoordCenter(tileCoord); + var projectionExtent = ol.tilegrid.extentFromProjection(projection); + if (!ol.extent.containsCoordinate(projectionExtent, center)) { + var worldWidth = ol.extent.getWidth(projectionExtent); + var worldsAway = Math.ceil((projectionExtent[0] - center[0]) / worldWidth); + center[0] += worldWidth * worldsAway; + return tileGrid.getTileCoordForCoordAndZ(center, z); + } else { + return tileCoord; } - return tileCoord; }; /** * @param {ol.TileCoord} tileCoord Tile coordinate. - * @param {ol.tilegrid.TileGrid} tileGrid Tile grid. - * @param {ol.proj.Projection} projection Projection. + * @param {!ol.tilegrid.TileGrid} tileGrid Tile grid. * @return {ol.TileCoord} Tile coordinate. */ -ol.tilecoord.clipX = function(tileCoord, tileGrid, projection) { +ol.tilecoord.restrictByExtentAndZ = function(tileCoord, tileGrid) { var z = tileCoord[0]; var x = tileCoord[1]; - var tileRange = tileGrid.getTileRange(z, projection); - return (x < tileRange.minX || x > tileRange.maxX) ? null : tileCoord; + var y = tileCoord[2]; + + if (tileGrid.getMinZoom() > z || z > tileGrid.getMaxZoom()) { + return null; + } + var extent = tileGrid.getExtent(); + var tileRange; + if (goog.isNull(extent)) { + tileRange = tileGrid.getFullTileRange(z); + } else { + tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z); + } + if (goog.isNull(tileRange)) { + return tileCoord; + } else { + return tileRange.containsXY(x, y) ? tileCoord : null; + } }; diff --git a/src/ol/tilegrid/tilegrid.js b/src/ol/tilegrid/tilegrid.js index 9c08e6ee9c..507fbbf614 100644 --- a/src/ol/tilegrid/tilegrid.js +++ b/src/ol/tilegrid/tilegrid.js @@ -4,6 +4,7 @@ goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.functions'); goog.require('goog.math'); +goog.require('goog.object'); goog.require('ol'); goog.require('ol.Coordinate'); goog.require('ol.TileCoord'); @@ -69,6 +70,10 @@ ol.tilegrid.TileGrid = function(options) { goog.asserts.assert(this.origins_.length == this.resolutions_.length, 'number of origins and resolutions must be equal'); } + if (goog.isNull(this.origins_) && goog.isNull(this.origin_) && + goog.isDef(options.extent)) { + this.origin_ = ol.extent.getBottomLeft(options.extent); + } goog.asserts.assert( (goog.isNull(this.origin_) && !goog.isNull(this.origins_)) || (!goog.isNull(this.origin_) && goog.isNull(this.origins_)), @@ -97,6 +102,51 @@ ol.tilegrid.TileGrid = function(options) { (!goog.isNull(this.tileSize_) && goog.isNull(this.tileSizes_)), 'either tileSize or tileSizes must be configured, never both'); + var extent = options.extent; + + /** + * @private + * @type {ol.Extent} + */ + this.extent_ = goog.isDef(extent) ? extent : null; + + /** + * @private + * @type {Array.} + */ + this.fullTileRanges_ = null; + + if (goog.isDef(options.sizes)) { + goog.asserts.assert(options.sizes.length == this.resolutions_.length, + 'number of sizes and resolutions must be equal'); + this.fullTileRanges_ = goog.array.map(options.sizes, function(size, z) { + goog.asserts.assert(size[0] > 0, 'width must be > 0'); + goog.asserts.assert(size[1] !== 0, 'height must not be 0'); + var tileRange = new ol.TileRange(0, size[0] - 1, 0, size[1] - 1); + if (tileRange.maxY < tileRange.minY) { + tileRange.minY = size[1]; + tileRange.maxY = -1; + } + if (this.minZoom <= z && z <= this.maxZoom && goog.isDef(extent)) { + goog.asserts.assert(tileRange.containsTileRange( + this.getTileRangeForExtentAndZ(extent, z)), + 'extent tile range must not exceed tilegrid width and height'); + } + return tileRange; + }, this); + } else if (goog.isDef(extent)) { + var extentWidth = ol.extent.getWidth(extent); + var extentHeight = ol.extent.getHeight(extent); + var fullTileRanges = new Array(this.resolutions_.length); + var tileSize; + for (var z = 0, zz = fullTileRanges.length; z < zz; ++z) { + tileSize = ol.size.toSize(this.getTileSize(z), this.tmpSize_); + fullTileRanges[z] = new ol.TileRange( + 0, Math.ceil(extentWidth / tileSize[0] / this.resolutions_[z]) - 1, + 0, Math.ceil(extentHeight / tileSize[1] / this.resolutions_[z]) - 1); + } + this.fullTileRanges_ = fullTileRanges; + } /** * @private @@ -104,17 +154,6 @@ ol.tilegrid.TileGrid = function(options) { */ this.tmpSize_ = [0, 0]; - /** - * @private - * @type {Array.} - */ - this.widths_ = null; - if (goog.isDef(options.widths)) { - this.widths_ = options.widths; - goog.asserts.assert(this.widths_.length == this.resolutions_.length, - 'number of widths and resolutions must be equal'); - } - }; @@ -161,6 +200,15 @@ ol.tilegrid.TileGrid.prototype.forEachTileCoordParentTileRange = }; +/** + * Get the extent for this tile grid, if it was configured. + * @return {ol.Extent} Extent. + */ +ol.tilegrid.TileGrid.prototype.getExtent = function() { + return this.extent_; +}; + + /** * Get the maximum zoom level for the grid. * @return {number} Max zoom. @@ -412,25 +460,6 @@ ol.tilegrid.TileGrid.prototype.getTileCoordResolution = function(tileCoord) { }; -/** - * @param {number} z Zoom level. - * @param {ol.proj.Projection} projection Projection. - * @param {ol.TileRange=} opt_tileRange Tile range. - * @return {ol.TileRange} Tile range. - */ -ol.tilegrid.TileGrid.prototype.getTileRange = - function(z, projection, opt_tileRange) { - var projectionExtentTileRange = this.getTileRangeForExtentAndZ( - ol.tilegrid.extentFromProjection(projection), z); - var width = this.getWidth(z); - if (!goog.isDef(width)) { - width = projectionExtentTileRange.getWidth(); - } - return ol.TileRange.createOrUpdate( - 0, width - 1, 0, projectionExtentTileRange.getHeight(), opt_tileRange); -}; - - /** * Get the tile size for a zoom level. The type of the return value matches the * `tileSize` or `tileSizes` that the tile grid was configured with. To always @@ -455,15 +484,16 @@ ol.tilegrid.TileGrid.prototype.getTileSize = function(z) { /** * @param {number} z Zoom level. - * @return {number|undefined} Width for the specified zoom level or `undefined` - * if unknown. + * @return {ol.TileRange} Extent tile range for the specified zoom level. */ -ol.tilegrid.TileGrid.prototype.getWidth = function(z) { - if (!goog.isNull(this.widths_)) { +ol.tilegrid.TileGrid.prototype.getFullTileRange = function(z) { + if (goog.isNull(this.fullTileRanges_)) { + return null; + } else { goog.asserts.assert(this.minZoom <= z && z <= this.maxZoom, 'z is not in allowed range (%s <= %s <= %s', this.minZoom, z, this.maxZoom); - return this.widths_[z]; + return this.fullTileRanges_[z]; } }; @@ -478,26 +508,6 @@ ol.tilegrid.TileGrid.prototype.getZForResolution = function(resolution) { }; -/** - * @param {number} z Zoom level. - * @param {ol.proj.Projection} projection Projection. - * @return {boolean} Whether the tile grid is defined for the whole globe when - * used with the provided `projection` at zoom level `z`. - */ -ol.tilegrid.TileGrid.prototype.isGlobal = function(z, projection) { - var width = this.getWidth(z); - if (goog.isDef(width)) { - var projTileGrid = ol.tilegrid.getForProjection(projection); - var projExtent = projection.getExtent(); - return ol.size.toSize(this.getTileSize(z), this.tmpSize_)[0] * width == - projTileGrid.getTileSize(z) * - projTileGrid.getTileRangeForExtentAndZ(projExtent, z).getWidth(); - } else { - return projection.isGlobal(); - } -}; - - /** * @param {ol.proj.Projection} projection Projection. * @return {ol.tilegrid.TileGrid} Default tile grid for the passed projection. @@ -611,3 +621,36 @@ ol.tilegrid.extentFromProjection = function(projection) { } return extent; }; + + +/** + * @param {ol.tilegrid.TileGrid} tileGrid Tile grid. + * @return {function(ol.TileCoord, ol.proj.Projection, ol.TileCoord=): + * ol.TileCoord} Tile coordinate transform. + */ +ol.tilegrid.createOriginTopLeftTileCoordTransform = function(tileGrid) { + goog.asserts.assert(!goog.isNull(tileGrid), 'tileGrid required'); + return ( + /** + * @param {ol.TileCoord} tileCoord Tile coordinate. + * @param {ol.proj.Projection} projection Projection. + * @param {ol.TileCoord=} opt_tileCoord Destination tile coordinate. + * @return {ol.TileCoord} Tile coordinate. + */ + function(tileCoord, projection, opt_tileCoord) { + if (goog.isNull(tileCoord)) { + return null; + } + var z = tileCoord[0]; + var fullTileRange = tileGrid.getFullTileRange(z); + var height; + if (goog.isNull(fullTileRange) || fullTileRange.minY < 0) { + height = 0; + } else { + height = fullTileRange.getHeight(); + } + return ol.tilecoord.createOrUpdate( + z, tileCoord[1], height - tileCoord[2] - 1, opt_tileCoord); + } + ); +}; diff --git a/src/ol/tilegrid/wmtstilegrid.js b/src/ol/tilegrid/wmtstilegrid.js index 6770358784..74838aa2e9 100644 --- a/src/ol/tilegrid/wmtstilegrid.js +++ b/src/ol/tilegrid/wmtstilegrid.js @@ -32,12 +32,13 @@ ol.tilegrid.WMTS = function(options) { // FIXME: should the matrixIds become optionnal? goog.base(this, { + extent: options.extent, origin: options.origin, origins: options.origins, resolutions: options.resolutions, tileSize: options.tileSize, tileSizes: options.tileSizes, - widths: options.widths + sizes: options.sizes }); }; @@ -69,11 +70,13 @@ ol.tilegrid.WMTS.prototype.getMatrixIds = function() { * Create a tile grid from a WMTS capabilities matrix set. * @param {Object} matrixSet An object representing a matrixSet in the * capabilities document. + * @param {ol.Extent=} opt_extent An optional extent to restrict the tile + * ranges the server provides. * @return {ol.tilegrid.WMTS} WMTS tileGrid instance. * @api */ ol.tilegrid.WMTS.createFromCapabilitiesMatrixSet = - function(matrixSet) { + function(matrixSet, opt_extent) { /** @type {!Array.} */ var resolutions = []; @@ -83,8 +86,8 @@ ol.tilegrid.WMTS.createFromCapabilitiesMatrixSet = var origins = []; /** @type {!Array.} */ var tileSizes = []; - /** @type {!Array.} */ - var widths = []; + /** @type {!Array.} */ + var sizes = []; var supportedCRSPropName = 'SupportedCRS'; var matrixIdsPropName = 'TileMatrix'; @@ -108,26 +111,30 @@ ol.tilegrid.WMTS.createFromCapabilitiesMatrixSet = goog.array.forEach(matrixSet[matrixIdsPropName], function(elt, index, array) { matrixIds.push(elt[identifierPropName]); + var resolution = elt[scaleDenominatorPropName] * 0.28E-3 / + metersPerUnit; + var tileWidth = elt[tileWidthPropName]; + var tileHeight = elt[tileHeightPropName]; + var matrixHeight = elt['MatrixHeight']; if (switchOriginXY) { origins.push([elt[topLeftCornerPropName][1], elt[topLeftCornerPropName][0]]); } else { origins.push(elt[topLeftCornerPropName]); } - resolutions.push(elt[scaleDenominatorPropName] * 0.28E-3 / - metersPerUnit); - var tileWidth = elt[tileWidthPropName]; - var tileHeight = elt[tileHeightPropName]; + resolutions.push(resolution); tileSizes.push(tileWidth == tileHeight ? tileWidth : [tileWidth, tileHeight]); - widths.push(elt['MatrixWidth']); + // top-left origin, so height is negative + sizes.push([elt['MatrixWidth'], -matrixHeight]); }); return new ol.tilegrid.WMTS({ + extent: opt_extent, origins: origins, resolutions: resolutions, matrixIds: matrixIds, tileSizes: tileSizes, - widths: widths + sizes: sizes }); }; diff --git a/test/spec/ol/source/tilesource.test.js b/test/spec/ol/source/tilesource.test.js index ddd2b610b1..b6c2a99030 100644 --- a/test/spec/ol/source/tilesource.test.js +++ b/test/spec/ol/source/tilesource.test.js @@ -115,22 +115,7 @@ describe('ol.source.Tile', function() { }); - describe('#getWrapXTileCoord()', function() { - - it('returns the expected tile coordinate - {wrapX: undefined}', function() { - var tileSource = new ol.source.Tile({ - projection: 'EPSG:3857' - }); - - var tileCoord = tileSource.getWrapXTileCoord([6, -31, 22]); - expect(tileCoord).to.eql([6, -31, 22]); - - tileCoord = tileSource.getWrapXTileCoord([6, 33, 22]); - expect(tileCoord).to.eql([6, 33, 22]); - - tileCoord = tileSource.getWrapXTileCoord([6, 97, 22]); - expect(tileCoord).to.eql([6, 97, 22]); - }); + describe('#getTileCoordForTileUrlFunction()', function() { it('returns the expected tile coordinate - {wrapX: true}', function() { var tileSource = new ol.source.Tile({ @@ -138,13 +123,13 @@ describe('ol.source.Tile', function() { wrapX: true }); - var tileCoord = tileSource.getWrapXTileCoord([6, -31, 22]); + var tileCoord = tileSource.getTileCoordForTileUrlFunction([6, -31, 22]); expect(tileCoord).to.eql([6, 33, 22]); - tileCoord = tileSource.getWrapXTileCoord([6, 33, 22]); + tileCoord = tileSource.getTileCoordForTileUrlFunction([6, 33, 22]); expect(tileCoord).to.eql([6, 33, 22]); - tileCoord = tileSource.getWrapXTileCoord([6, 97, 22]); + tileCoord = tileSource.getTileCoordForTileUrlFunction([6, 97, 22]); expect(tileCoord).to.eql([6, 33, 22]); }); @@ -154,13 +139,13 @@ describe('ol.source.Tile', function() { wrapX: false }); - var tileCoord = tileSource.getWrapXTileCoord([6, -31, 22]); + var tileCoord = tileSource.getTileCoordForTileUrlFunction([6, -31, 22]); expect(tileCoord).to.eql(null); - tileCoord = tileSource.getWrapXTileCoord([6, 33, 22]); + tileCoord = tileSource.getTileCoordForTileUrlFunction([6, 33, 22]); expect(tileCoord).to.eql([6, 33, 22]); - tileCoord = tileSource.getWrapXTileCoord([6, 97, 22]); + tileCoord = tileSource.getTileCoordForTileUrlFunction([6, 97, 22]); expect(tileCoord).to.eql(null); }); }); diff --git a/test/spec/ol/source/xyzsource.test.js b/test/spec/ol/source/xyzsource.test.js index e5d7cab36c..866f3c20b5 100644 --- a/test/spec/ol/source/xyzsource.test.js +++ b/test/spec/ol/source/xyzsource.test.js @@ -66,15 +66,18 @@ describe('ol.source.XYZ', function() { it('returns the expected URL', function() { var projection = xyzTileSource.getProjection(); var tileUrl = xyzTileSource.tileUrlFunction( - xyzTileSource.getWrapXTileCoord([6, -31, -23], projection)); + xyzTileSource.getTileCoordForTileUrlFunction( + [6, -31, 41], projection)); expect(tileUrl).to.eql('6/33/22'); tileUrl = xyzTileSource.tileUrlFunction( - xyzTileSource.getWrapXTileCoord([6, 33, -23], projection)); + xyzTileSource.getTileCoordForTileUrlFunction( + [6, 33, 41], projection)); expect(tileUrl).to.eql('6/33/22'); tileUrl = xyzTileSource.tileUrlFunction( - xyzTileSource.getWrapXTileCoord([6, 97, -23], projection)); + xyzTileSource.getTileCoordForTileUrlFunction( + [6, 97, 41], projection)); expect(tileUrl).to.eql('6/33/22'); }); @@ -83,14 +86,20 @@ describe('ol.source.XYZ', function() { describe('crop y', function() { it('returns the expected URL', function() { + var projection = xyzTileSource.getProjection(); var tileUrl = xyzTileSource.tileUrlFunction( - [6, 33, -87]); + xyzTileSource.getTileCoordForTileUrlFunction( + [6, 33, 150], projection)); expect(tileUrl).to.be(undefined); - tileUrl = xyzTileSource.tileUrlFunction([6, 33, -23]); + tileUrl = xyzTileSource.tileUrlFunction( + xyzTileSource.getTileCoordForTileUrlFunction( + [6, 33, 41], projection)); expect(tileUrl).to.eql('6/33/22'); - tileUrl = xyzTileSource.tileUrlFunction([6, 33, 41]); + tileUrl = xyzTileSource.tileUrlFunction( + xyzTileSource.getTileCoordForTileUrlFunction( + [6, 33, -23], projection)); expect(tileUrl).to.be(undefined); }); diff --git a/test/spec/ol/tilecoord.test.js b/test/spec/ol/tilecoord.test.js index d46c3483c8..f2bd42411d 100644 --- a/test/spec/ol/tilecoord.test.js +++ b/test/spec/ol/tilecoord.test.js @@ -46,7 +46,79 @@ describe('ol.TileCoord', function() { ol.tilecoord.hash(tileCoord2)); }); }); + + describe('restrictByExtentAndZ', function() { + + it('restricts by z', function() { + var tileGrid = new ol.tilegrid.TileGrid({ + extent: [10, 20, 30, 40], + tileSize: 10, + resolutions: [2, 1], + minZoom: 1 + }); + expect(ol.tilecoord.restrictByExtentAndZ([0, 0, 0], tileGrid)) + .to.equal(null); + expect(ol.tilecoord.restrictByExtentAndZ([1, 0, 0], tileGrid)) + .to.eql([1, 0, 0]); + expect(ol.tilecoord.restrictByExtentAndZ([2, 0, 0], tileGrid)) + .to.equal(null); + }); + + it('restricts by extent when extent defines tile ranges', function() { + var tileGrid = new ol.tilegrid.TileGrid({ + extent: [10, 20, 30, 40], + sizes: [[3, 3]], + tileSize: 10, + resolutions: [1] + }); + expect(ol.tilecoord.restrictByExtentAndZ([0, 1, 1], tileGrid)) + .to.eql([0, 1, 1]); + expect(ol.tilecoord.restrictByExtentAndZ([0, 2, 0], tileGrid)) + .to.equal(null); + expect(ol.tilecoord.restrictByExtentAndZ([0, 0, 2], tileGrid)) + .to.equal(null); + }); + + it('restricts by extent when sizes define tile ranges', function() { + var tileGrid = new ol.tilegrid.TileGrid({ + origin: [10, 20], + sizes: [[3, 3]], + tileSize: 10, + resolutions: [1] + }); + expect(ol.tilecoord.restrictByExtentAndZ([0, 0, 0], tileGrid)) + .to.eql([0, 0, 0]); + expect(ol.tilecoord.restrictByExtentAndZ([0, -1, 0], tileGrid)) + .to.equal(null); + expect(ol.tilecoord.restrictByExtentAndZ([0, 0, -1], tileGrid)) + .to.equal(null); + expect(ol.tilecoord.restrictByExtentAndZ([0, 2, 2], tileGrid)) + .to.eql([0, 2, 2]); + expect(ol.tilecoord.restrictByExtentAndZ([0, 3, 0], tileGrid)) + .to.equal(null); + expect(ol.tilecoord.restrictByExtentAndZ([0, 0, 3], tileGrid)) + .to.equal(null); + }); + + it('does not restrict by extent with no extent or sizes', function() { + var tileGrid = new ol.tilegrid.TileGrid({ + origin: [10, 20], + tileSize: 10, + resolutions: [1] + }); + expect(ol.tilecoord.restrictByExtentAndZ([0, Infinity, 0], tileGrid)) + .to.eql([0, Infinity, 0]); + expect(ol.tilecoord.restrictByExtentAndZ([0, 0, Infinity], tileGrid)) + .to.eql([0, 0, Infinity]); + expect(ol.tilecoord.restrictByExtentAndZ([0, -Infinity, 0], tileGrid)) + .to.eql([0, -Infinity, 0]); + expect(ol.tilecoord.restrictByExtentAndZ([0, 0, Infinity], tileGrid)) + .to.eql([0, 0, Infinity]); + }); + }); + }); goog.require('ol.TileCoord'); goog.require('ol.tilecoord'); +goog.require('ol.tilegrid.TileGrid'); diff --git a/test/spec/ol/tilegrid/tilegrid.test.js b/test/spec/ol/tilegrid/tilegrid.test.js index 14f8b14d83..e476a58726 100644 --- a/test/spec/ol/tilegrid/tilegrid.test.js +++ b/test/spec/ol/tilegrid/tilegrid.test.js @@ -150,6 +150,129 @@ describe('ol.tilegrid.TileGrid', function() { }); }); + describe('create with extent exceeding tile ranges', function() { + it('throws an exception', function() { + expect(function() { + return new ol.tilegrid.TileGrid({ + extent: [10, 20, 30, 40], + sizes: [[1, 1]], + tileSize: 10, + resolutions: [1] + }); + }).to.throwException(); + expect(function() { + return new ol.tilegrid.TileGrid({ + extent: [10, 20, 30, 40], + origin: [10, 40], // top-left origin + sizes: [[3, 3]], // would have to be [[3, -3]] for this to not throw + tileSize: 10, + resolutions: [1] + }); + }).to.throwException(); + }); + }); + + describe('create with origin', function() { + var tileGrid; + beforeEach(function() { + tileGrid = new ol.tilegrid.TileGrid({ + origin: [10, 20], + tileSize: 10, + resolutions: [1] + }); + }); + + it('returns the configured origin', function() { + expect(tileGrid.getOrigin()).to.eql([10, 20]); + }); + + it('returns null for an unknown extent', function() { + expect(tileGrid.getExtent()).to.equal(null); + }); + + it('returns null for an unknown full tile range', function() { + expect(tileGrid.getFullTileRange(0)).to.equal(null); + }); + }); + + describe('create with extent', function() { + var tileGrid; + beforeEach(function() { + tileGrid = new ol.tilegrid.TileGrid({ + extent: [10, 20, 30, 40], + tileSize: 10, + resolutions: [1] + }); + }); + + it('assumes bottom left corner of extent as origin', function() { + expect(tileGrid.getOrigin()).to.eql([10, 20]); + }); + + it('calculates full tile ranges from extent', function() { + var fullTileRange = tileGrid.getFullTileRange(0); + expect(fullTileRange.minX).to.equal(0); + expect(fullTileRange.maxX).to.equal(1); + expect(fullTileRange.minY).to.equal(0); + expect(fullTileRange.maxY).to.equal(1); + }); + }); + + describe('create with extent and sizes', function() { + var tileGrid; + beforeEach(function() { + tileGrid = new ol.tilegrid.TileGrid({ + extent: [10, 20, 30, 40], + sizes: [[3, 3]], + tileSize: 10, + resolutions: [1] + }); + }); + + it('returns the configured extent', function() { + expect(tileGrid.getExtent()).to.eql([10, 20, 30, 40]); + }); + + it('calculates full tile ranges from sizes', function() { + var fullTileRange = tileGrid.getFullTileRange(0); + expect(fullTileRange.minX).to.equal(0); + expect(fullTileRange.maxX).to.equal(2); + expect(fullTileRange.minY).to.equal(0); + expect(fullTileRange.maxY).to.equal(2); + }); + }); + + describe('create with top-left origin and sizes', function() { + var tileGrid; + beforeEach(function() { + tileGrid = new ol.tilegrid.TileGrid({ + origin: [10, 40], + sizes: [[3, -3]], + tileSize: 10, + resolutions: [1] + }); + }); + + it('calculates correct minX and maxX for negative heights', function() { + var fullTileRange = tileGrid.getFullTileRange(0); + expect(fullTileRange.minY).to.equal(-3); + expect(fullTileRange.maxY).to.equal(-1); + }); + }); + + describe('create with extent and origin', function() { + it('uses both origin and extent', function() { + var tileGrid = new ol.tilegrid.TileGrid({ + origin: [0, 0], + extent: [10, 20, 30, 40], + tileSize: 10, + resolutions: [1] + }); + expect(tileGrid.getOrigin()).to.eql([0, 0]); + expect(tileGrid.getExtent()).to.eql([10, 20, 30, 40]); + }); + }); + describe('createForExtent', function() { it('allows creation of tile grid from extent', function() { var extent = ol.extent.createOrUpdate(-100, -100, 100, 100); @@ -247,6 +370,78 @@ describe('ol.tilegrid.TileGrid', function() { }); + describe('createOriginTopLeftTileCoordTransform', function() { + + it('transforms y to -y-1 for top-left origins', function() { + var tileGrid = new ol.tilegrid.TileGrid({ + origin: [10, 40], + sizes: [[2, -2], [4, -4]], + resolutions: [1, 0.5], + tileSize: 10 + }); + var transformFn = + ol.tilegrid.createOriginTopLeftTileCoordTransform(tileGrid); + expect(transformFn([0, 0, -2])).to.eql([0, 0, 1]); + expect(transformFn([0, 0, -1])).to.eql([0, 0, 0]); + expect(transformFn([1, 0, -4])).to.eql([1, 0, 3]); + expect(transformFn([1, 0, -1])).to.eql([1, 0, 0]); + }); + + it('transforms y to -y-1 when origin corner is not specified', function() { + var tileGrid1 = new ol.tilegrid.TileGrid({ + origin: [10, 20], + resolutions: [1, 0.5], + tileSize: 10 + }); + var tileGrid = new ol.tilegrid.TileGrid({ + origin: [10, 40], + resolutions: [1, 0.5], + tileSize: 10 + }); + var transformFn1 = + ol.tilegrid.createOriginTopLeftTileCoordTransform(tileGrid); + var transformFn2 = + ol.tilegrid.createOriginTopLeftTileCoordTransform(tileGrid1); + expect(transformFn1([0, 0, -2])).to.eql([0, 0, 1]); + expect(transformFn2([0, 0, -2])).to.eql([0, 0, 1]); + expect(transformFn1([0, 0, -1])).to.eql([0, 0, 0]); + expect(transformFn2([0, 0, -1])).to.eql([0, 0, 0]); + expect(transformFn1([1, 0, -4])).to.eql([1, 0, 3]); + expect(transformFn2([1, 0, -4])).to.eql([1, 0, 3]); + expect(transformFn1([1, 0, -1])).to.eql([1, 0, 0]); + expect(transformFn2([1, 0, -1])).to.eql([1, 0, 0]); + }); + + it('transforms y to height-y-1 for bottom-left origins', function() { + var tileGrid1 = new ol.tilegrid.TileGrid({ + extent: [10, 20, 30, 40], + resolutions: [1, 0.5], + tileSize: 10 + }); + var tileGrid2 = new ol.tilegrid.TileGrid({ + origin: [10, 20], + sizes: [[2, 2], [4, 4]], + resolutions: [1, 0.5], + tileSize: 10 + }); + var transformFn1 = + ol.tilegrid.createOriginTopLeftTileCoordTransform(tileGrid1); + var transformFn2 = + ol.tilegrid.createOriginTopLeftTileCoordTransform(tileGrid2); + expect(tileGrid1.getFullTileRange(0).getHeight()).to.equal(2); + expect(transformFn1([0, 0, 0])).to.eql([0, 0, 1]); + expect(transformFn2([0, 0, 0])).to.eql([0, 0, 1]); + expect(transformFn1([0, 0, 1])).to.eql([0, 0, 0]); + expect(transformFn2([0, 0, 1])).to.eql([0, 0, 0]); + expect(tileGrid1.getFullTileRange(1).getHeight()).to.equal(4); + expect(transformFn1([1, 0, 0])).to.eql([1, 0, 3]); + expect(transformFn2([1, 0, 0])).to.eql([1, 0, 3]); + expect(transformFn1([1, 0, 3])).to.eql([1, 0, 0]); + expect(transformFn2([1, 0, 3])).to.eql([1, 0, 0]); + }); + + }); + describe('getForProjection', function() { it('gets the default tile grid for a projection', function() { @@ -839,7 +1034,7 @@ describe('ol.tilegrid.TileGrid', function() { }); }); - describe('getZForResolution (approcimate)', function() { + describe('getZForResolution (approximate)', function() { it('returns the expected z value', function() { var tileGrid = new ol.tilegrid.TileGrid({ resolutions: resolutions, @@ -874,4 +1069,5 @@ goog.require('ol.proj'); goog.require('ol.proj.EPSG3857'); goog.require('ol.proj.Projection'); goog.require('ol.proj.Units'); +goog.require('ol.TileRange'); goog.require('ol.tilegrid.TileGrid'); diff --git a/test/spec/ol/tilegrid/wmtstilegrid.test.js b/test/spec/ol/tilegrid/wmtstilegrid.test.js index 1e03436c23..966dacb59e 100644 --- a/test/spec/ol/tilegrid/wmtstilegrid.test.js +++ b/test/spec/ol/tilegrid/wmtstilegrid.test.js @@ -42,27 +42,17 @@ describe('ol.tilegrid.WMTS', function() { expect(tileGrid.origins_).to.be.an('array'); expect(tileGrid.origins_).to.have.length(20); expect(tileGrid.origins_).to.eql( - [[-20037508.3428, 20037508.3428], [-20037508.3428, 20037508.3428], - [-20037508.3428, 20037508.3428], [-20037508.3428, 20037508.3428], - [-20037508.3428, 20037508.3428], [-20037508.3428, 20037508.3428], - [-20037508.3428, 20037508.3428], [-20037508.3428, 20037508.3428], - [-20037508.3428, 20037508.3428], [-20037508.3428, 20037508.3428], - [-20037508.3428, 20037508.3428], [-20037508.3428, 20037508.3428], - [-20037508.3428, 20037508.3428], [-20037508.3428, 20037508.3428], - [-20037508.3428, 20037508.3428], [-20037508.3428, 20037508.3428], - [-20037508.3428, 20037508.3428], [-20037508.3428, 20037508.3428], - [-20037508.3428, 20037508.3428], [-20037508.3428, 20037508.3428] - ]); + goog.array.repeat([-20037508.3428, 20037508.3428], 20)); expect(tileGrid.tileSizes_).to.be.an('array'); expect(tileGrid.tileSizes_).to.have.length(20); expect(tileGrid.tileSizes_).to.eql( - [256, 256, 256, 256, 256, 256, 256, 256, 256, 256, - 256, 256, 256, 256, 256, 256, 256, 256, 256, 256]); + goog.array.repeat(256, 20)); }); }); }); +goog.require('goog.array'); goog.require('ol.format.WMTSCapabilities'); goog.require('ol.tilegrid.WMTS');