Merge pull request #1494 from twpayne/vector-api-wms-hidpi

[vector-api] HiDPI (Retina) support for WMS single image layers
This commit is contained in:
Tom Payne
2014-01-09 06:39:00 -08:00
13 changed files with 540 additions and 274 deletions

View File

@@ -51,15 +51,27 @@ ol.extent.boundingExtentXYs_ = function(xs, ys, opt_extent) {
/**
* Increase an extent by the provided value.
* @param {ol.Extent} extent The extent to buffer.
* Return extent increased by the provided value.
* @param {ol.Extent} extent Extent.
* @param {number} value The amount by wich the extent should be buffered.
* @param {ol.Extent=} opt_extent Extent.
* @return {ol.Extent} Extent.
*/
ol.extent.buffer = function(extent, value) {
extent[0] -= value;
extent[1] -= value;
extent[2] += value;
extent[3] += value;
ol.extent.buffer = function(extent, value, opt_extent) {
if (goog.isDef(opt_extent)) {
opt_extent[0] = extent[0] - value;
opt_extent[1] = extent[1] - value;
opt_extent[2] = extent[2] + value;
opt_extent[3] = extent[3] + value;
return opt_extent;
} else {
return [
extent[0] - value,
extent[1] - value,
extent[2] + value,
extent[3] + value
];
}
};

View File

@@ -64,6 +64,7 @@ ol.renderer.canvas.ImageLayer.prototype.getImageTransform = function() {
ol.renderer.canvas.ImageLayer.prototype.prepareFrame =
function(frameState, layerState) {
var devicePixelRatio = frameState.devicePixelRatio;
var view2DState = frameState.view2DState;
var viewCenter = view2DState.center;
var viewResolution = view2DState.resolution;
@@ -79,7 +80,7 @@ ol.renderer.canvas.ImageLayer.prototype.prepareFrame =
if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.INTERACTING]) {
image = imageSource.getImage(frameState.extent, viewResolution,
frameState.devicePixelRatio, view2DState.projection);
devicePixelRatio, view2DState.projection);
if (!goog.isNull(image)) {
var imageState = image.getState();
if (imageState == ol.ImageState.IDLE) {
@@ -95,16 +96,17 @@ ol.renderer.canvas.ImageLayer.prototype.prepareFrame =
if (!goog.isNull(this.image_)) {
image = this.image_;
var imageExtent = image.getExtent();
var imageResolution = image.getResolution() / image.getPixelRatio();
var devicePixelRatio = frameState.devicePixelRatio;
var imageResolution = image.getResolution();
var imagePixelRatio = image.getPixelRatio();
var scale = devicePixelRatio * imageResolution /
(viewResolution * imagePixelRatio);
ol.vec.Mat4.makeTransform2D(this.imageTransform_,
devicePixelRatio * frameState.size[0] / 2,
devicePixelRatio * frameState.size[1] / 2,
devicePixelRatio * imageResolution / viewResolution,
devicePixelRatio * imageResolution / viewResolution,
scale, scale,
viewRotation,
(imageExtent[0] - viewCenter[0]) / imageResolution,
(viewCenter[1] - imageExtent[3]) / imageResolution);
imagePixelRatio * (imageExtent[0] - viewCenter[0]) / imageResolution,
imagePixelRatio * (viewCenter[1] - imageExtent[3]) / imageResolution);
this.updateAttributions(frameState.attributions, image.getAttributions());
this.updateLogos(frameState, imageSource);
}

View File

@@ -52,10 +52,10 @@ ol.source.Image = function(options) {
ol.ImageUrlFunction.nullImageUrlFunction;
/**
* @private
* @protected
* @type {?string}
*/
this.crossOrigin_ =
this.crossOrigin =
goog.isDef(options.crossOrigin) ? options.crossOrigin : null;
/**
@@ -89,7 +89,7 @@ ol.source.Image.prototype.createImage =
var imageUrl = this.imageUrlFunction(extent, size, projection);
if (goog.isDef(imageUrl)) {
image = new ol.Image(
extent, resolution, pixelRatio, imageUrl, this.crossOrigin_,
extent, resolution, pixelRatio, imageUrl, this.crossOrigin,
this.getAttributions());
}
return image;

View File

@@ -1,3 +1,4 @@
@exportSymbol ol.source.ImageWMS
@exportProperty ol.source.ImageWMS.prototype.getParams
@exportProperty ol.source.ImageWMS.prototype.setUrl
@exportProperty ol.source.ImageWMS.prototype.updateParams

View File

@@ -2,21 +2,39 @@ goog.provide('ol.source.ImageWMS');
goog.require('goog.asserts');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.uri.utils');
goog.require('ol.Image');
goog.require('ol.ImageUrlFunction');
goog.require('ol.extent');
goog.require('ol.source.Image');
goog.require('ol.source.wms');
goog.require('ol.source.wms.ServerType');
/**
* @constructor
* @extends {ol.source.Image}
* @param {olx.source.ImageWMSOptions} options Options.
* @param {olx.source.ImageWMSOptions=} opt_options Options.
* @todo stability experimental
*/
ol.source.ImageWMS = function(options) {
ol.source.ImageWMS = function(opt_options) {
var options = goog.isDef(opt_options) ? opt_options : {};
goog.base(this, {
attributions: options.attributions,
crossOrigin: options.crossOrigin,
extent: options.extent,
projection: options.projection,
resolutions: options.resolutions
});
/**
* @private
* @type {string|undefined}
*/
this.url_ = options.url;
/**
* @private
@@ -24,19 +42,12 @@ ol.source.ImageWMS = function(options) {
*/
this.params_ = options.params;
var imageUrlFunction = goog.isDef(options.url) ?
ol.ImageUrlFunction.createFromParamsFunction(
options.url, this.params_, ol.source.wms.getUrl) :
ol.ImageUrlFunction.nullImageUrlFunction;
goog.base(this, {
attributions: options.attributions,
crossOrigin: options.crossOrigin,
extent: options.extent,
projection: options.projection,
resolutions: options.resolutions,
imageUrlFunction: imageUrlFunction
});
/**
* @private
* @type {boolean}
*/
this.v13_ = true;
this.updateV13_();
/**
* @private
@@ -81,8 +92,16 @@ ol.source.ImageWMS.prototype.getParams = function() {
*/
ol.source.ImageWMS.prototype.getImage =
function(extent, resolution, pixelRatio, projection) {
if (!goog.isDef(this.url_)) {
return null;
}
resolution = this.findNearestResolution(resolution);
pixelRatio = this.hidpi_ ? pixelRatio : 1;
if (pixelRatio != 1 && (!this.hidpi_ || !goog.isDef(this.serverType_))) {
pixelRatio = 1;
}
var image = this.image_;
if (!goog.isNull(image) &&
@@ -92,20 +111,92 @@ ol.source.ImageWMS.prototype.getImage =
return image;
}
extent = extent.slice();
ol.extent.scaleFromCenter(extent, this.ratio_);
var width = (extent[2] - extent[0]) / resolution;
var height = (extent[3] - extent[1]) / resolution;
var size = [width * pixelRatio, height * pixelRatio];
var params = {
'SERVICE': 'WMS',
'VERSION': ol.source.wms.DEFAULT_VERSION,
'REQUEST': 'GetMap',
'FORMAT': 'image/png',
'TRANSPARENT': true
};
goog.object.extend(params, this.params_);
if (goog.isDef(this.serverType_) && pixelRatio > 1) {
var param = ol.source.wms.getDpiParam(this.serverType_, pixelRatio);
goog.object.extend(this.params_, param);
params[this.v13_ ? 'CRS' : 'SRS'] = projection.getCode();
if (!('STYLES' in this.params_)) {
goog.object.set(params, 'STYLES', new String(''));
}
this.image_ = this.createImage(
extent, resolution, pixelRatio, size, projection);
if (pixelRatio != 1) {
switch (this.serverType_) {
case ol.source.wms.ServerType.GEOSERVER:
var dpi = (90 * pixelRatio + 0.5) | 0;
goog.object.set(params, 'FORMAT_OPTIONS', 'dpi:' + dpi);
break;
case ol.source.wms.ServerType.MAPSERVER:
goog.object.set(params, 'MAP_RESOLUTION', 90 * pixelRatio);
break;
case ol.source.wms.ServerType.QGIS:
goog.object.set(params, 'DPI', 90 * pixelRatio);
break;
default:
goog.asserts.fail();
break;
}
}
extent = extent.slice();
var centerX = (extent[0] + extent[2]) / 2;
var centerY = (extent[1] + extent[3]) / 2;
if (this.ratio_ != 1) {
var halfWidth = this.ratio_ * (extent[2] - extent[0]) / 2;
var halfHeight = this.ratio_ * (extent[3] - extent[1]) / 2;
extent[0] = centerX - halfWidth;
extent[1] = centerY - halfHeight;
extent[2] = centerX + halfWidth;
extent[3] = centerY + halfHeight;
}
var imageResolution = resolution / pixelRatio;
// Compute an integer width and height.
var width = Math.ceil((extent[2] - extent[0]) / imageResolution);
goog.object.set(params, 'WIDTH', width);
var height = Math.ceil((extent[3] - extent[1]) / imageResolution);
goog.object.set(params, 'HEIGHT', height);
// Modify the extent to match the integer width and height.
extent[0] = centerX - imageResolution * width / 2;
extent[2] = centerX + imageResolution * width / 2;
extent[1] = centerY - imageResolution * height / 2;
extent[3] = centerY + imageResolution * height / 2;
var axisOrientation = projection.getAxisOrientation();
var bbox;
if (this.v13_ && axisOrientation.substr(0, 2) == 'ne') {
bbox = [extent[1], extent[0], extent[3], extent[2]];
} else {
bbox = extent;
}
goog.object.set(params, 'BBOX', bbox.join(','));
var url = goog.uri.utils.appendParamsFromMap(this.url_, params);
this.image_ = new ol.Image(extent, resolution, pixelRatio, url,
this.crossOrigin, this.getAttributions());
return this.image_;
};
/**
* @param {string|undefined} url URL.
*/
ol.source.ImageWMS.prototype.setUrl = function(url) {
if (url != this.url_) {
this.url_ = url;
this.image_ = null;
this.dispatchChangeEvent();
}
};
@@ -115,6 +206,17 @@ ol.source.ImageWMS.prototype.getImage =
*/
ol.source.ImageWMS.prototype.updateParams = function(params) {
goog.object.extend(this.params_, params);
this.updateV13_();
this.image_ = null;
this.dispatchChangeEvent();
};
/**
* @private
*/
ol.source.ImageWMS.prototype.updateV13_ = function() {
var version =
goog.object.get(this.params_, 'VERSION', ol.source.wms.DEFAULT_VERSION);
this.v13_ = goog.string.compareVersions(version, '1.3') >= 0;
};

View File

@@ -1,10 +1,13 @@
// FIXME add minZoom support
// FIXME add date line wrap (tile coord transform)
goog.provide('ol.source.TileWMS');
goog.require('goog.array');
goog.require('goog.math');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.uri.utils');
goog.require('ol.TileCoord');
goog.require('ol.TileUrlFunction');
goog.require('ol.extent');
@@ -16,22 +19,40 @@ goog.require('ol.source.wms');
/**
* @constructor
* @extends {ol.source.TileImage}
* @param {olx.source.TileWMSOptions} options Tile WMS options.
* @param {olx.source.TileWMSOptions=} opt_options Tile WMS options.
* @todo stability experimental
*/
ol.source.TileWMS = function(options) {
ol.source.TileWMS = function(opt_options) {
var tileGrid;
if (goog.isDef(options.tileGrid)) {
tileGrid = options.tileGrid;
}
var options = goog.isDef(opt_options) ? opt_options : {};
var params = goog.isDef(options.params) ? options.params : {};
var transparent = goog.object.get(params, 'TRANSPARENT', true);
goog.base(this, {
attributions: options.attributions,
crossOrigin: options.crossOrigin,
extent: options.extent,
logo: options.logo,
opaque: !transparent,
projection: options.projection,
tileGrid: options.tileGrid,
tileLoadFunction: options.tileLoadFunction,
tileUrlFunction: goog.bind(this.tileUrlFunction_, this)
});
var tileUrlFunction = ol.TileUrlFunction.nullTileUrlFunction;
var urls = options.urls;
if (!goog.isDef(urls) && goog.isDef(options.url)) {
urls = ol.TileUrlFunction.expandUrl(options.url);
}
/**
* @private
* @type {Array.<string>|undefined}
*/
this.urls_ = urls;
/**
* @private
* @type {number}
@@ -42,7 +63,13 @@ ol.source.TileWMS = function(options) {
* @private
* @type {Object}
*/
this.params_ = options.params;
this.params_ = params;
/**
* @private
* @type {boolean}
*/
this.v13_ = true;
/**
* @private
@@ -51,62 +78,13 @@ ol.source.TileWMS = function(options) {
this.coordKeyPrefix_ = '';
this.resetCoordKeyPrefix_();
if (goog.isDef(urls)) {
var tileUrlFunctions = goog.array.map(
urls, function(url) {
return ol.TileUrlFunction.createFromParamsFunction(
url, this.params_, this.gutter_, ol.source.wms.getUrl);
}, this);
tileUrlFunction = ol.TileUrlFunction.createFromTileUrlFunctions(
tileUrlFunctions);
}
/**
* @private
* @type {ol.Extent}
*/
this.tmpExtent_ = ol.extent.createEmpty();
var transparent = goog.isDef(options.params['TRANSPARENT']) ?
options.params['TRANSPARENT'] : true;
var extent = options.extent;
var tileCoordTransform = function(tileCoord, projection) {
var tileGrid = this.getTileGrid();
if (goog.isNull(tileGrid)) {
tileGrid = ol.tilegrid.getForProjection(projection);
}
if (tileGrid.getResolutions().length <= tileCoord.z) {
return null;
}
var x = tileCoord.x;
var tileExtent = tileGrid.getTileCoordExtent(tileCoord);
var projectionExtent = projection.getExtent();
extent = goog.isDef(extent) ? extent : projectionExtent;
if (!goog.isNull(extent) && projection.isGlobal() &&
extent[0] === projectionExtent[0] &&
extent[2] === projectionExtent[2]) {
var numCols = Math.ceil(
(extent[2] - extent[0]) /
(tileExtent[2] - tileExtent[0]));
x = goog.math.modulo(x, numCols);
tileExtent = tileGrid.getTileCoordExtent(
new ol.TileCoord(tileCoord.z, x, tileCoord.y));
}
if (!goog.isNull(extent) && (!ol.extent.intersects(tileExtent, extent) ||
ol.extent.touches(tileExtent, extent))) {
return null;
}
return new ol.TileCoord(tileCoord.z, x, tileCoord.y);
};
goog.base(this, {
attributions: options.attributions,
crossOrigin: options.crossOrigin,
extent: extent,
logo: options.logo,
tileGrid: options.tileGrid,
opaque: !transparent,
projection: options.projection,
tileLoadFunction: options.tileLoadFunction,
tileUrlFunction: ol.TileUrlFunction.withTileCoordTransform(
tileCoordTransform, tileUrlFunction)
});
this.updateV13_();
};
goog.inherits(ol.source.TileWMS, ol.source.TileImage);
@@ -152,6 +130,83 @@ ol.source.TileWMS.prototype.resetCoordKeyPrefix_ = function() {
};
/**
* @param {ol.TileCoord} tileCoord Tile coordinate.
* @param {ol.proj.Projection} projection Projection.
* @private
* @return {string|undefined} Tile URL.
*/
ol.source.TileWMS.prototype.tileUrlFunction_ = function(tileCoord, projection) {
var urls = this.urls_;
if (!goog.isDef(urls) || goog.array.isEmpty(urls)) {
return undefined;
}
var tileGrid = this.getTileGrid();
if (goog.isNull(tileGrid)) {
tileGrid = ol.tilegrid.getForProjection(projection);
}
if (tileGrid.getResolutions().length <= tileCoord.z) {
return undefined;
}
var tileExtent = tileGrid.getTileCoordExtent(tileCoord);
var params = {
'SERVICE': 'WMS',
'VERSION': ol.source.wms.DEFAULT_VERSION,
'REQUEST': 'GetMap',
'FORMAT': 'image/png',
'TRANSPARENT': true
};
goog.object.extend(params, this.params_);
var tileResolution = tileGrid.getResolution(tileCoord.z);
var tileSize = tileGrid.getTileSize(tileCoord.z);
var gutter = this.gutter_;
if (gutter === 0) {
goog.object.set(params, 'WIDTH', tileSize[0]);
goog.object.set(params, 'HEIGHT', tileSize[1]);
} else {
goog.object.set(params, 'WIDTH', tileSize[0] + 2 * gutter);
goog.object.set(params, 'HEIGHT', tileSize[1] + 2 * gutter);
tileExtent =
ol.extent.buffer(tileExtent, tileResolution * gutter, this.tmpExtent_);
}
params[this.v13_ ? 'CRS' : 'SRS'] = projection.getCode();
if (!('STYLES' in this.params_)) {
goog.object.set(params, 'STYLES', new String(''));
}
var axisOrientation = projection.getAxisOrientation();
var bbox;
if (this.v13_ && axisOrientation.substr(0, 2) == 'ne') {
bbox = this.tmpExtent_;
bbox[0] = tileExtent[1];
bbox[1] = tileExtent[0];
bbox[2] = tileExtent[3];
bbox[3] = tileExtent[2];
} else {
bbox = tileExtent;
}
goog.object.set(params, 'BBOX', bbox.join(','));
var url;
if (urls.length == 1) {
url = urls[0];
} else {
var index = goog.math.modulo(tileCoord.hash(), this.urls_.length);
url = urls[index];
}
return goog.uri.utils.appendParamsFromMap(url, params);
};
/**
* Update the user-provided params.
* @param {Object} params Params.
@@ -160,5 +215,16 @@ ol.source.TileWMS.prototype.resetCoordKeyPrefix_ = function() {
ol.source.TileWMS.prototype.updateParams = function(params) {
goog.object.extend(this.params_, params);
this.resetCoordKeyPrefix_();
this.updateV13_();
this.dispatchChangeEvent();
};
/**
* @private
*/
ol.source.TileWMS.prototype.updateV13_ = function() {
var version =
goog.object.get(this.params_, 'VERSION', ol.source.wms.DEFAULT_VERSION);
this.v13_ = goog.string.compareVersions(version, '1.3') >= 0;
};

View File

@@ -1,9 +1,11 @@
goog.provide('ol.source.wms');
goog.provide('ol.source.wms.ServerType');
goog.require('goog.asserts');
goog.require('goog.object');
goog.require('goog.uri.utils');
/**
* @define {string} WMS default version.
*/
ol.source.wms.DEFAULT_VERSION = '1.3.0';
/**
@@ -14,60 +16,3 @@ ol.source.wms.ServerType = {
MAPSERVER: 'mapserver',
QGIS: 'qgis'
};
/**
* @param {string} baseUrl WMS base URL.
* @param {Object.<string, string|number>} params Request parameters.
* @param {ol.Extent} extent Extent.
* @param {ol.Size} size Size.
* @param {ol.proj.Projection} projection Projection.
* @return {string} WMS GetMap request URL.
*/
ol.source.wms.getUrl = function(baseUrl, params, extent, size, projection) {
var baseParams = {
'SERVICE': 'WMS',
'VERSION': '1.3.0',
'REQUEST': 'GetMap',
'FORMAT': 'image/png',
'TRANSPARENT': true,
'WIDTH': Math.round(size[0]),
'HEIGHT': Math.round(size[1])
};
goog.object.extend(baseParams, params);
//TODO: Provide our own appendParams function to avoid this empty string hack
var stylesParam = 'STYLES';
baseParams[stylesParam] = params[stylesParam] || new String('');
var wms13 = baseParams['VERSION'] > '1.3';
baseParams[wms13 ? 'CRS' : 'SRS'] = projection.getCode();
var axisOrientation = projection.getAxisOrientation();
var bboxValues = (wms13 && axisOrientation.substr(0, 2) == 'ne') ?
[extent[1], extent[0], extent[3], extent[2]] :
[extent[0], extent[1], extent[2], extent[3]];
baseParams['BBOX'] = bboxValues.join(',');
return goog.uri.utils.appendParamsFromMap(baseUrl, baseParams);
};
/**
* @param {ol.source.wms.ServerType} serverType Server name.
* @param {number} pixelRatio Pixel ratio.
* @return {Object.<string, string>}
*/
ol.source.wms.getDpiParam = function(serverType, pixelRatio) {
var param = {};
if (serverType == ol.source.wms.ServerType.MAPSERVER) {
param['MAP_RESOLUTION'] = 90 * pixelRatio;
} else if (serverType == ol.source.wms.ServerType.GEOSERVER) {
param['FORMAT_OPTIONS'] = 'dpi:' + 90 * pixelRatio;
} else if (serverType == ol.source.wms.ServerType.QGIS) {
param['DPI'] = 90 * pixelRatio;
} else {
goog.asserts.fail();
}
return param;
};

View File

@@ -4,7 +4,6 @@ goog.provide('ol.TileUrlFunctionType');
goog.require('goog.array');
goog.require('goog.math');
goog.require('ol.TileCoord');
goog.require('ol.extent');
/**
@@ -82,46 +81,6 @@ ol.TileUrlFunction.createFromTileUrlFunctions = function(tileUrlFunctions) {
};
/**
* @param {string} baseUrl Base URL (may have query data).
* @param {Object.<string,*>} params Params to encode in the URL.
* @param {number} gutter Gutter value.
* @param {function(this: ol.source.TileImage, string, Object.<string,*>,
* ol.Extent, ol.Size, ol.proj.Projection)} paramsFunction params function.
* @return {ol.TileUrlFunctionType} Tile URL function.
*/
ol.TileUrlFunction.createFromParamsFunction =
function(baseUrl, params, gutter, paramsFunction) {
var tmpExtent = ol.extent.createEmpty();
var tmpSize = [0, 0];
return (
/**
* @this {ol.source.TileImage}
* @param {ol.TileCoord} tileCoord Tile Coordinate.
* @param {ol.proj.Projection} projection Projection.
* @return {string|undefined} Tile URL.
*/
function(tileCoord, projection) {
if (goog.isNull(tileCoord)) {
return undefined;
} else {
var tileGrid = this.getTileGrid();
if (goog.isNull(tileGrid)) {
tileGrid = ol.tilegrid.getForProjection(projection);
}
var tileResolution = tileGrid.getResolution(tileCoord.z);
var tileSize = tileGrid.getTileSize(tileCoord.z);
tmpSize[0] = tileSize[0] + (2 * gutter);
tmpSize[1] = tileSize[1] + (2 * gutter);
var extent = tileGrid.getTileCoordExtent(tileCoord, tmpExtent);
ol.extent.buffer(extent, tileResolution * gutter);
return paramsFunction.call(this, baseUrl, params,
extent, tmpSize, projection);
}
});
};
/**
* @this {ol.source.TileImage}
* @param {ol.TileCoord} tileCoord Tile coordinate.