From 3e18b85206c2481d6f31d1a893ce04d4b1edf1f8 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 22 Mar 2015 14:27:19 +0100 Subject: [PATCH] Add ol.source.Tile support for wrapping around the x-axis --- examples/wms-tiled-wrap-180.html | 51 ++++++++++++++++++++++++++ examples/wms-tiled-wrap-180.js | 28 ++++++++++++++ externs/olx.js | 40 ++++++++++++++++++-- src/ol/source/bingmapssource.js | 11 ++---- src/ol/source/tilearcgisrestsource.js | 3 +- src/ol/source/tileimagesource.js | 7 +++- src/ol/source/tilejsonsource.js | 14 ++----- src/ol/source/tilesource.js | 35 +++++++++++++++++- src/ol/source/tilewmssource.js | 3 +- src/ol/source/xyzsource.js | 7 ++-- src/ol/tilecoord.js | 35 ++++++++++++++++++ src/ol/tilegrid/tilegrid.js | 3 +- src/ol/tilegrid/xyztilegrid.js | 6 --- test/spec/ol/source/tilesource.test.js | 50 +++++++++++++++++++++++++ test/spec/ol/source/xyzsource.test.js | 9 +++-- 15 files changed, 260 insertions(+), 42 deletions(-) create mode 100644 examples/wms-tiled-wrap-180.html create mode 100644 examples/wms-tiled-wrap-180.js diff --git a/examples/wms-tiled-wrap-180.html b/examples/wms-tiled-wrap-180.html new file mode 100644 index 0000000000..01aeb64c69 --- /dev/null +++ b/examples/wms-tiled-wrap-180.html @@ -0,0 +1,51 @@ + + + + + + + + + + + Tiled WMS wrap 180° meridian example + + + + + +
+ +
+
+
+
+
+ +
+ +
+

Tiled WMS wrap 180° meridian example

+

Example of a tiled WMS layer that wraps across the 180° meridian.

+
+

See the wms-tiled-wrap-180.js source to see how this is done.

+
+
wms, tile, dateline, wrap, 180
+
+ +
+ +
+ + + + + + + diff --git a/examples/wms-tiled-wrap-180.js b/examples/wms-tiled-wrap-180.js new file mode 100644 index 0000000000..3b9ea8c53e --- /dev/null +++ b/examples/wms-tiled-wrap-180.js @@ -0,0 +1,28 @@ +goog.require('ol.Map'); +goog.require('ol.View'); +goog.require('ol.layer.Tile'); +goog.require('ol.source.MapQuest'); +goog.require('ol.source.TileWMS'); + + +var layers = [ + new ol.layer.Tile({ + source: new ol.source.MapQuest({layer: 'sat'}) + }), + new ol.layer.Tile({ + source: new ol.source.TileWMS(/** @type {olx.source.TileWMSOptions} */ ({ + url: 'http://demo.boundlessgeo.com/geoserver/ne/wms', + params: {'LAYERS': 'ne:ne_10m_admin_0_countries', 'TILED': true}, + serverType: 'geoserver', + wrapX: true + })) + }) +]; +var map = new ol.Map({ + layers: layers, + target: 'map', + view: new ol.View({ + center: [0, 0], + zoom: 1 + }) +}); diff --git a/externs/olx.js b/externs/olx.js index 41bb3c50aa..be41aa7676 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -3861,7 +3861,8 @@ olx.source.TileUTFGridOptions.prototype.url; * tileGrid: (ol.tilegrid.TileGrid|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined), * tilePixelRatio: (number|undefined), - * tileUrlFunction: (ol.TileUrlFunctionType|undefined)}} + * tileUrlFunction: (ol.TileUrlFunctionType|undefined), + * wrapX: (boolean|undefined)}} * @api */ olx.source.TileImageOptions; @@ -3964,6 +3965,17 @@ olx.source.TileImageOptions.prototype.tilePixelRatio; olx.source.TileImageOptions.prototype.tileUrlFunction; +/** + * Whether to wrap the world horizontally. The default, `undefined`, is to + * request out-of-bounds tiles from the server. 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. + * @type {boolean|undefined} + * @api + */ +olx.source.TileImageOptions.prototype.wrapX; + + /** * @typedef {{attributions: (Array.|undefined), * format: ol.format.Feature, @@ -5040,7 +5052,8 @@ olx.source.ServerVectorOptions.prototype.projection; * projection: ol.proj.ProjectionLike, * tileLoadFunction: (ol.TileLoadFunctionType|undefined), * url: (string|undefined), - * urls: (Array.|undefined)}} + * urls: (Array.|undefined), + * wrapX: (boolean|undefined)}} * @api */ olx.source.TileArcGISRestOptions; @@ -5111,6 +5124,14 @@ olx.source.TileArcGISRestOptions.prototype.tileLoadFunction; olx.source.TileArcGISRestOptions.prototype.url; +/** + * Whether to wrap the world horizontally. Default is `true`. + * @type {boolean|undefined} + * @api + */ +olx.source.TileArcGISRestOptions.prototype.wrapX; + + /** * ArcGIS Rest service urls. Use this instead of `url` when the ArcGIS Service supports multiple * urls for export requests. @@ -5190,7 +5211,8 @@ olx.source.TileJSONOptions.prototype.wrapX; * serverType: (ol.source.wms.ServerType|string|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined), * url: (string|undefined), - * urls: (Array.|undefined)}} + * urls: (Array.|undefined), + * wrapX: (boolean|undefined)}} * @api */ olx.source.TileWMSOptions; @@ -5320,6 +5342,18 @@ olx.source.TileWMSOptions.prototype.url; 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. + * @type {boolean|undefined} + * @api + */ +olx.source.TileWMSOptions.prototype.wrapX; + + /** * @typedef {{attributions: (Array.|undefined), * features: (Array.|undefined), diff --git a/src/ol/source/bingmapssource.js b/src/ol/source/bingmapssource.js index b2a9bff0fc..7dd58cd724 100644 --- a/src/ol/source/bingmapssource.js +++ b/src/ol/source/bingmapssource.js @@ -33,7 +33,8 @@ ol.source.BingMaps = function(options) { opaque: true, projection: ol.proj.get('EPSG:3857'), state: ol.source.State.LOADING, - tileLoadFunction: options.tileLoadFunction + tileLoadFunction: options.tileLoadFunction, + wrapX: goog.isDef(options.wrapX) ? options.wrapX : true }); /** @@ -48,12 +49,6 @@ ol.source.BingMaps = function(options) { */ this.maxZoom_ = goog.isDef(options.maxZoom) ? options.maxZoom : -1; - /** - * @private - * @type {boolean} - */ - this.wrapX_ = goog.isDef(options.wrapX) ? options.wrapX : true; - var protocol = ol.IS_HTTPS ? 'https:' : 'http:'; var uri = new goog.Uri( protocol + '//dev.virtualearth.net/REST/v1/Imagery/Metadata/' + @@ -117,7 +112,7 @@ ol.source.BingMaps.prototype.handleImageryMetadataResponse = var culture = this.culture_; this.tileUrlFunction = ol.TileUrlFunction.withTileCoordTransform( - tileGrid.createTileCoordTransform({wrapX: this.wrapX_}), + tileGrid.createTileCoordTransform(), ol.TileUrlFunction.createFromTileUrlFunctions( goog.array.map( resource.imageUrlSubdomains, diff --git a/src/ol/source/tilearcgisrestsource.js b/src/ol/source/tilearcgisrestsource.js index a0c387ad8a..b07281c88e 100644 --- a/src/ol/source/tilearcgisrestsource.js +++ b/src/ol/source/tilearcgisrestsource.js @@ -42,7 +42,8 @@ ol.source.TileArcGISRest = function(opt_options) { projection: options.projection, tileGrid: options.tileGrid, tileLoadFunction: options.tileLoadFunction, - tileUrlFunction: goog.bind(this.tileUrlFunction_, this) + tileUrlFunction: goog.bind(this.tileUrlFunction_, this), + wrapX: goog.isDef(options.wrapX) ? options.wrapX : true }); var urls = options.urls; diff --git a/src/ol/source/tileimagesource.js b/src/ol/source/tileimagesource.js index 9b1babe5d2..dc453c6eb1 100644 --- a/src/ol/source/tileimagesource.js +++ b/src/ol/source/tileimagesource.js @@ -34,7 +34,8 @@ ol.source.TileImage = function(options) { state: goog.isDef(options.state) ? /** @type {ol.source.State} */ (options.state) : undefined, tileGrid: options.tileGrid, - tilePixelRatio: options.tilePixelRatio + tilePixelRatio: options.tilePixelRatio, + wrapX: options.wrapX }); /** @@ -91,7 +92,9 @@ ol.source.TileImage.prototype.getTile = } else { goog.asserts.assert(projection); var tileCoord = [z, x, y]; - var tileUrl = this.tileUrlFunction(tileCoord, pixelRatio, projection); + var urlTileCoord = this.getWrapXTileCoord(tileCoord, projection); + var tileUrl = goog.isNull(urlTileCoord) ? undefined : + this.tileUrlFunction(urlTileCoord, pixelRatio, projection); var tile = new this.tileClass( tileCoord, goog.isDef(tileUrl) ? ol.TileState.IDLE : ol.TileState.EMPTY, diff --git a/src/ol/source/tilejsonsource.js b/src/ol/source/tilejsonsource.js index 4246908224..0cfc7e2df5 100644 --- a/src/ol/source/tilejsonsource.js +++ b/src/ol/source/tilejsonsource.js @@ -37,15 +37,10 @@ ol.source.TileJSON = function(options) { crossOrigin: options.crossOrigin, projection: ol.proj.get('EPSG:3857'), state: ol.source.State.LOADING, - tileLoadFunction: options.tileLoadFunction + tileLoadFunction: options.tileLoadFunction, + wrapX: goog.isDef(options.wrapX) ? options.wrapX : true }); - /** - * @type {boolean|undefined} - * @private - */ - this.wrapX_ = options.wrapX; - var request = new goog.net.Jsonp(options.url); request.send(undefined, goog.bind(this.handleTileJSONResponse, this)); @@ -82,10 +77,7 @@ ol.source.TileJSON.prototype.handleTileJSONResponse = function(tileJSON) { this.tileGrid = tileGrid; this.tileUrlFunction = ol.TileUrlFunction.withTileCoordTransform( - tileGrid.createTileCoordTransform({ - extent: extent, - wrapX: this.wrapX_ - }), + tileGrid.createTileCoordTransform({extent: extent}), ol.TileUrlFunction.createFromTemplates(tileJSON.tiles)); if (goog.isDef(tileJSON.attribution) && diff --git a/src/ol/source/tilesource.js b/src/ol/source/tilesource.js index ec360628b5..78c81d8071 100644 --- a/src/ol/source/tilesource.js +++ b/src/ol/source/tilesource.js @@ -20,7 +20,8 @@ goog.require('ol.tilegrid.TileGrid'); * tilePixelRatio: (number|undefined), * projection: ol.proj.ProjectionLike, * state: (ol.source.State|undefined), - * tileGrid: (ol.tilegrid.TileGrid|undefined)}} + * tileGrid: (ol.tilegrid.TileGrid|undefined), + * wrapX: (boolean|undefined)}} */ ol.source.TileOptions; @@ -72,6 +73,12 @@ ol.source.Tile = function(options) { */ this.tileCache = new ol.TileCache(); + /** + * @private + * @type {boolean|undefined} + */ + this.wrapX_ = options.wrapX; + }; goog.inherits(ol.source.Tile, ol.source.Source); @@ -203,6 +210,32 @@ ol.source.Tile.prototype.getTilePixelSize = }; +/** + * Handles x-axis wrapping. When `this.wrapX_` is undefined or the projection + * is not a global projection, `tileCoord` will be returned unaltered. When + * `this.wrapX_` is true, the tile coordinate will be wrapped horizontally. + * When `this.wrapX_` is `false`, `null` will be returned for tiles that are + * outside the projection extent. + * @param {ol.TileCoord} tileCoord Tile coordinate. + * @param {ol.proj.Projection=} opt_projection Projection. + * @return {ol.TileCoord} Tile coordinate. + */ +ol.source.Tile.prototype.getWrapXTileCoord = + function(tileCoord, opt_projection) { + var projection = goog.isDef(opt_projection) ? + opt_projection : this.getProjection(); + if (goog.isDef(this.wrapX_) && projection.isGlobal()) { + var tileGrid = this.getTileGridForProjection(projection); + var extent = ol.tilegrid.extentFromProjection(projection); + return this.wrapX_ ? + ol.tilecoord.wrapX(tileCoord, tileGrid, extent) : + ol.tilecoord.clipX(tileCoord, tileGrid, extent); + } else { + return tileCoord; + } +}; + + /** * Marks a tile coord as being used, without triggering a load. * @param {number} z Tile coordinate z. diff --git a/src/ol/source/tilewmssource.js b/src/ol/source/tilewmssource.js index a1873ad99f..82c067d687 100644 --- a/src/ol/source/tilewmssource.js +++ b/src/ol/source/tilewmssource.js @@ -47,7 +47,8 @@ ol.source.TileWMS = function(opt_options) { projection: options.projection, tileGrid: options.tileGrid, tileLoadFunction: options.tileLoadFunction, - tileUrlFunction: goog.bind(this.tileUrlFunction_, this) + tileUrlFunction: goog.bind(this.tileUrlFunction_, this), + wrapX: options.wrapX }); var urls = options.urls; diff --git a/src/ol/source/xyzsource.js b/src/ol/source/xyzsource.js index 1b5d2ec2d9..1615743e0c 100644 --- a/src/ol/source/xyzsource.js +++ b/src/ol/source/xyzsource.js @@ -34,16 +34,15 @@ ol.source.XYZ = function(options) { tileGrid: tileGrid, tileLoadFunction: options.tileLoadFunction, tilePixelRatio: options.tilePixelRatio, - tileUrlFunction: ol.TileUrlFunction.nullTileUrlFunction + tileUrlFunction: ol.TileUrlFunction.nullTileUrlFunction, + wrapX: goog.isDef(options.wrapX) ? options.wrapX : true }); /** * @private * @type {ol.TileCoordTransformType} */ - this.tileCoordTransform_ = tileGrid.createTileCoordTransform({ - wrapX: options.wrapX - }); + this.tileCoordTransform_ = tileGrid.createTileCoordTransform(); if (goog.isDef(options.tileUrlFunction)) { this.setTileUrlFunction(options.tileUrlFunction); diff --git a/src/ol/tilecoord.js b/src/ol/tilecoord.js index 25fcaeeba2..1ddc43b005 100644 --- a/src/ol/tilecoord.js +++ b/src/ol/tilecoord.js @@ -3,6 +3,7 @@ goog.provide('ol.tilecoord'); goog.require('goog.array'); goog.require('goog.asserts'); +goog.require('goog.math'); /** @@ -137,3 +138,37 @@ ol.tilecoord.quadKey = function(tileCoord) { ol.tilecoord.toString = function(tileCoord) { return ol.tilecoord.getKeyZXY(tileCoord[0], tileCoord[1], tileCoord[2]); }; + + +/** + * @param {ol.TileCoord} tileCoord Tile coordinate. + * @param {ol.tilegrid.TileGrid} tilegrid Tile grid. + * @param {ol.Extent} extent Extent. + * @return {ol.TileCoord} Tile coordinate. + */ +ol.tilecoord.wrapX = (function() { + var tmpTileCoord = [0, 0, 0]; + return function(tileCoord, tileGrid, extent) { + var z = tileCoord[0]; + var x = tileCoord[1]; + var tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z); + if (x < tileRange.minX || x > tileRange.maxX) { + x = goog.math.modulo(x, tileRange.getWidth()); + return ol.tilecoord.createOrUpdate(z, x, tileCoord[2], tmpTileCoord); + } + return tileCoord; + }; +})(); + + +/** + * @param {ol.TileCoord} tileCoord Tile coordinate. + * @param {ol.tilegrid.TileGrid} tileGrid Tile grid. + * @param {ol.Extent} extent Extent. + * @return {ol.TileCoord} Tile coordinate. + */ +ol.tilecoord.clipX = function(tileCoord, tileGrid, extent) { + var x = tileCoord[1]; + var tileRange = tileGrid.getTileRangeForExtentAndZ(extent, tileCoord[0]); + return (x < tileRange.minX || x > tileRange.maxX) ? null : tileCoord; +}; diff --git a/src/ol/tilegrid/tilegrid.js b/src/ol/tilegrid/tilegrid.js index e24e8e02b4..d49ca06e3e 100644 --- a/src/ol/tilegrid/tilegrid.js +++ b/src/ol/tilegrid/tilegrid.js @@ -103,8 +103,7 @@ ol.tilegrid.TileGrid.tmpTileCoord_ = [0, 0, 0]; /** * Returns the identity function. May be overridden in subclasses. - * @param {{extent: (ol.Extent|undefined), - * wrapX: (boolean|undefined)}=} opt_options Options. + * @param {{extent: (ol.Extent|undefined)}=} opt_options Options. * @return {function(ol.TileCoord, ol.proj.Projection, ol.TileCoord=): * ol.TileCoord} Tile coordinate transform. */ diff --git a/src/ol/tilegrid/xyztilegrid.js b/src/ol/tilegrid/xyztilegrid.js index 399df2b860..6fec56bcd6 100644 --- a/src/ol/tilegrid/xyztilegrid.js +++ b/src/ol/tilegrid/xyztilegrid.js @@ -47,7 +47,6 @@ ol.tilegrid.XYZ.prototype.createTileCoordTransform = function(opt_options) { var options = goog.isDef(opt_options) ? opt_options : {}; var minZ = this.minZoom; var maxZ = this.maxZoom; - var wrapX = goog.isDef(options.wrapX) ? options.wrapX : true; /** @type {Array.} */ var tileRangeByZ = null; if (goog.isDef(options.extent)) { @@ -75,11 +74,6 @@ ol.tilegrid.XYZ.prototype.createTileCoordTransform = function(opt_options) { } var n = Math.pow(2, z); var x = tileCoord[1]; - if (wrapX) { - x = goog.math.modulo(x, n); - } else if (x < 0 || n <= x) { - return null; - } var y = tileCoord[2]; if (y < -n || -1 < y) { return null; diff --git a/test/spec/ol/source/tilesource.test.js b/test/spec/ol/source/tilesource.test.js index 3024a1cfcb..ddd2b610b1 100644 --- a/test/spec/ol/source/tilesource.test.js +++ b/test/spec/ol/source/tilesource.test.js @@ -115,6 +115,56 @@ 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]); + }); + + it('returns the expected tile coordinate - {wrapX: true}', function() { + var tileSource = new ol.source.Tile({ + projection: 'EPSG:3857', + wrapX: true + }); + + var tileCoord = tileSource.getWrapXTileCoord([6, -31, 22]); + expect(tileCoord).to.eql([6, 33, 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, 33, 22]); + }); + + it('returns the expected tile coordinate - {wrapX: false}', function() { + var tileSource = new ol.source.Tile({ + projection: 'EPSG:3857', + wrapX: false + }); + + var tileCoord = tileSource.getWrapXTileCoord([6, -31, 22]); + expect(tileCoord).to.eql(null); + + tileCoord = tileSource.getWrapXTileCoord([6, 33, 22]); + expect(tileCoord).to.eql([6, 33, 22]); + + tileCoord = tileSource.getWrapXTileCoord([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 1a9ed0c1b2..e5d7cab36c 100644 --- a/test/spec/ol/source/xyzsource.test.js +++ b/test/spec/ol/source/xyzsource.test.js @@ -64,14 +64,17 @@ describe('ol.source.XYZ', function() { describe('wrap x', function() { it('returns the expected URL', function() { + var projection = xyzTileSource.getProjection(); var tileUrl = xyzTileSource.tileUrlFunction( - [6, -31, -23]); + xyzTileSource.getWrapXTileCoord([6, -31, -23], projection)); expect(tileUrl).to.eql('6/33/22'); - tileUrl = xyzTileSource.tileUrlFunction([6, 33, -23]); + tileUrl = xyzTileSource.tileUrlFunction( + xyzTileSource.getWrapXTileCoord([6, 33, -23], projection)); expect(tileUrl).to.eql('6/33/22'); - tileUrl = xyzTileSource.tileUrlFunction([6, 97, -23]); + tileUrl = xyzTileSource.tileUrlFunction( + xyzTileSource.getWrapXTileCoord([6, 97, -23], projection)); expect(tileUrl).to.eql('6/33/22'); });