Add ol.source.Tile support for wrapping around the x-axis

This commit is contained in:
Andreas Hocevar
2015-03-22 14:27:19 +01:00
parent c707c5e9db
commit 3e18b85206
15 changed files with 260 additions and 42 deletions

View File

@@ -0,0 +1,51 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width">
<link rel="stylesheet" href="../css/ol.css" type="text/css">
<link rel="stylesheet" href="../resources/bootstrap/css/bootstrap.min.css" type="text/css">
<link rel="stylesheet" href="../resources/layout.css" type="text/css">
<link rel="stylesheet" href="../resources/bootstrap/css/bootstrap-responsive.min.css" type="text/css">
<title>Tiled WMS wrap 180° meridian example</title>
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<a class="brand" href="./"><img src="../resources/logo.png"> OpenLayers 3 Examples</a>
</div>
</div>
</div>
<div class="container-fluid">
<div class="row-fluid">
<div class="span12">
<div id="map" class="map"></div>
</div>
</div>
<div class="row-fluid">
<div class="span12">
<h4 id="title">Tiled WMS wrap 180° meridian example</h4>
<p id="shortdesc">Example of a tiled WMS layer that wraps across the 180° meridian.</p>
<div id="docs">
<p>See the <a href="wms-tiled-wrap-180.js" target="_blank">wms-tiled-wrap-180.js source</a> to see how this is done.</p>
</div>
<div id="tags">wms, tile, dateline, wrap, 180</div>
</div>
</div>
</div>
<script src="../resources/jquery.min.js" type="text/javascript"></script>
<script src="../resources/example-behaviour.js" type="text/javascript"></script>
<script src="loader.js?id=wms-tiled-wrap-180" type="text/javascript"></script>
</body>
</html>

View File

@@ -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
})
});

View File

@@ -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.<ol.Attribution>|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.<string>|undefined)}}
* urls: (Array.<string>|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.<string>|undefined)}}
* urls: (Array.<string>|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.<ol.Attribution>|undefined),
* features: (Array.<ol.Feature>|undefined),

View File

@@ -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,

View File

@@ -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;

View File

@@ -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,

View File

@@ -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) &&

View File

@@ -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.

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;
};

View File

@@ -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.
*/

View File

@@ -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.<ol.TileRange>} */
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;

View File

@@ -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);
});
});
});

View File

@@ -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');
});