diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc index 59a9c6701b..f6fba884dc 100644 --- a/src/objectliterals.jsdoc +++ b/src/objectliterals.jsdoc @@ -744,6 +744,15 @@ * @property {ol.Extent|undefined} extent Extent. * @property {olx.source.WMSGetFeatureInfoOptions|undefined} getFeatureInfoOptions * Options for GetFeatureInfo. + * @property {number|undefined} gutter The size in pixels of the + * gutter around image tiles to ignore. By setting this property to a + * non-zero value, images will be requested that are wider and taller than + * the tile size by a value of `2 x gutter`. Defaults to zero. Using a + * non-zero value allows artifacts of rendering at tile edges to be ignored. If + * you control the WMS service it is recommended to address "artifacts at tile + * edges" issues by properly configuring the WMS service. For example, MapServer + * has a `tile_map_edge_buffer` configuration parameter for this. See + * http://mapserver.org/output/tile_mode.html. * @property {string|undefined} logo Logo. * @property {ol.tilegrid.TileGrid|undefined} tileGrid Tile grid. * @property {number|undefined} maxZoom Maximum zoom. diff --git a/src/ol/renderer/canvas/canvastilelayerrenderer.js b/src/ol/renderer/canvas/canvastilelayerrenderer.js index 52d0c14213..d445fbba87 100644 --- a/src/ol/renderer/canvas/canvastilelayerrenderer.js +++ b/src/ol/renderer/canvas/canvastilelayerrenderer.js @@ -180,6 +180,7 @@ ol.renderer.canvas.TileLayer.prototype.renderFrame = if (goog.isNull(tileGrid)) { tileGrid = ol.tilegrid.getForProjection(projection); } + var tileGutter = tileSource.getGutter(); var z = tileGrid.getZForResolution(view2DState.resolution); var tileSize = tileGrid.getTileSize(z); var tileResolution = tileGrid.getResolution(z); @@ -338,7 +339,9 @@ ol.renderer.canvas.TileLayer.prototype.renderFrame = context.clearRect(x, y, tileSize[0], tileSize[1]); } if (tileState == ol.TileState.LOADED) { - context.drawImage(tile.getImage(), x, y); + context.drawImage(tile.getImage(), + tileGutter, tileGutter, tileSize[0], tileSize[1], + x, y, tileSize[0], tileSize[1]); } this.renderedTiles_[index] = tile; } @@ -357,7 +360,9 @@ ol.renderer.canvas.TileLayer.prototype.renderFrame = context.clearRect(x, y, width, height); } if (tileState == ol.TileState.LOADED) { - context.drawImage(tile.getImage(), x, y, width, height); + context.drawImage(tile.getImage(), + tileGutter, tileGutter, tileSize[0], tileSize[1], + x, y, width, height); } interimTileRange = tileGrid.getTileRangeForExtentAndZ(tileExtent, z, tmpTileRange); diff --git a/src/ol/renderer/dom/domtilelayerrenderer.js b/src/ol/renderer/dom/domtilelayerrenderer.js index 68733fbc16..467efe6200 100644 --- a/src/ol/renderer/dom/domtilelayerrenderer.js +++ b/src/ol/renderer/dom/domtilelayerrenderer.js @@ -97,6 +97,7 @@ ol.renderer.dom.TileLayer.prototype.renderFrame = if (goog.isNull(tileGrid)) { tileGrid = ol.tilegrid.getForProjection(projection); } + var tileGutter = tileSource.getGutter(); var z = tileGrid.getZForResolution(view2DState.resolution); var tileResolution = tileGrid.getResolution(z); var center = view2DState.center; @@ -185,7 +186,7 @@ ol.renderer.dom.TileLayer.prototype.renderFrame = } tilesToDraw = tilesToDrawByZ[tileLayerZKey]; for (tileCoordKey in tilesToDraw) { - tileLayerZ.addTile(tilesToDraw[tileCoordKey]); + tileLayerZ.addTile(tilesToDraw[tileCoordKey], tileGutter); } tileLayerZ.finalizeAddTiles(); } @@ -320,8 +321,9 @@ ol.renderer.dom.TileLayerZ_ = function(tileGrid, tileCoordOrigin) { /** * @param {ol.Tile} tile Tile. + * @param {number} tileGutter Tile gutter. */ -ol.renderer.dom.TileLayerZ_.prototype.addTile = function(tile) { +ol.renderer.dom.TileLayerZ_.prototype.addTile = function(tile, tileGutter) { var tileCoord = tile.tileCoord; goog.asserts.assert(tileCoord.z == this.tileCoordOrigin_.z); var tileCoordKey = tileCoord.toString(); @@ -330,20 +332,36 @@ ol.renderer.dom.TileLayerZ_.prototype.addTile = function(tile) { } var tileSize = this.tileGrid_.getTileSize(tileCoord.z); var image = tile.getImage(this); - var style = image.style; - // Bootstrap sets the style max-width: 100% for all images, which breaks - // prevents the tile from being displayed in FireFox. Workaround by - // overriding the max-width style. - style.maxWidth = 'none'; - style.position = 'absolute'; - style.left = + var imageStyle = image.style; + // Bootstrap sets the style max-width: 100% for all images, which + // prevents the tile from being displayed in FireFox. Workaround + // by overriding the max-width style. + imageStyle.maxWidth = 'none'; + var tileElement; + var tileElementStyle; + if (tileGutter > 0) { + tileElement = goog.dom.createElement(goog.dom.TagName.DIV); + tileElementStyle = tileElement.style; + tileElementStyle.overflow = 'hidden'; + tileElementStyle.width = tileSize[0] + 'px'; + tileElementStyle.height = tileSize[1] + 'px'; + imageStyle.position = 'absolute'; + imageStyle.left = -tileGutter + 'px'; + imageStyle.top = -tileGutter + 'px'; + goog.dom.appendChild(tileElement, image); + } else { + tileElement = image; + tileElementStyle = imageStyle; + } + tileElementStyle.position = 'absolute'; + tileElementStyle.left = ((tileCoord.x - this.tileCoordOrigin_.x) * tileSize[0]) + 'px'; - style.top = + tileElementStyle.top = ((this.tileCoordOrigin_.y - tileCoord.y) * tileSize[1]) + 'px'; if (goog.isNull(this.documentFragment_)) { this.documentFragment_ = document.createDocumentFragment(); } - goog.dom.appendChild(this.documentFragment_, image); + goog.dom.appendChild(this.documentFragment_, tileElement); this.tiles_[tileCoordKey] = tile; }; diff --git a/src/ol/renderer/webgl/webglmaprenderer.js b/src/ol/renderer/webgl/webglmaprenderer.js index 3880918334..1dc503ef90 100644 --- a/src/ol/renderer/webgl/webglmaprenderer.js +++ b/src/ol/renderer/webgl/webglmaprenderer.js @@ -75,6 +75,26 @@ ol.renderer.webgl.Map = function(container, map) { this.canvas_.className = ol.css.CLASS_UNSELECTABLE; goog.dom.insertChildAt(container, this.canvas_, 0); + /** + * @private + * @type {HTMLCanvasElement} + */ + this.clipTileCanvas_ = /** @type {HTMLCanvasElement} */ + (goog.dom.createElement(goog.dom.TagName.CANVAS)); + + /** + * @private + * @type {ol.Size} + */ + this.clipTileCanvasSize_ = [0, 0]; + + /** + * @private + * @type {CanvasRenderingContext2D} + */ + this.clipTileContext_ = /** @type {CanvasRenderingContext2D} */ + (this.clipTileCanvas_.getContext('2d')); + /** * @private * @type {boolean} @@ -185,9 +205,14 @@ ol.renderer.webgl.Map = function(container, map) { function(map, frameState) { if (!this.tileTextureQueue_.isEmpty()) { this.tileTextureQueue_.reprioritize(); - var tile = - /** @type {ol.Tile} */ (this.tileTextureQueue_.dequeue()[0]); - this.bindTileTexture(tile, goog.webgl.LINEAR, goog.webgl.LINEAR); + var element = this.tileTextureQueue_.dequeue(); + var tile = /** @type {ol.Tile} */ (element[0]); + var tileWidth = /** @type {number} */ (element[3]); + var tileHeight = /** @type {number} */ (element[4]); + var tileGutter = /** @type {number} */ (element[5]); + this.bindTileTexture(tile, + tileWidth, tileHeight, tileGutter, + goog.webgl.LINEAR, goog.webgl.LINEAR); } }, this); @@ -246,11 +271,14 @@ ol.renderer.webgl.Map.prototype.bindBuffer = function(target, buf) { /** * @param {ol.Tile} tile Tile. + * @param {number} tileWidth Tile width. + * @param {number} tileHeight Tile height. + * @param {number} tileGutter Tile gutter. * @param {number} magFilter Mag filter. * @param {number} minFilter Min filter. */ ol.renderer.webgl.Map.prototype.bindTileTexture = - function(tile, magFilter, minFilter) { + function(tile, tileWidth, tileHeight, tileGutter, magFilter, minFilter) { var gl = this.getGL(); var tileKey = tile.getKey(); if (this.textureCache_.containsKey(tileKey)) { @@ -269,8 +297,29 @@ ol.renderer.webgl.Map.prototype.bindTileTexture = } else { var texture = gl.createTexture(); gl.bindTexture(goog.webgl.TEXTURE_2D, texture); - gl.texImage2D(goog.webgl.TEXTURE_2D, 0, goog.webgl.RGBA, goog.webgl.RGBA, - goog.webgl.UNSIGNED_BYTE, tile.getImage()); + if (tileGutter > 0) { + var clipTileCanvas = this.clipTileCanvas_; + var clipTileCanvasSize = this.clipTileCanvasSize_; + var clipTileContext = this.clipTileContext_; + if (clipTileCanvasSize[0] != tileWidth || + clipTileCanvasSize[1] != tileHeight) { + clipTileCanvas.width = tileWidth; + clipTileCanvas.height = tileHeight; + clipTileCanvasSize[0] = tileWidth; + clipTileCanvasSize[1] = tileHeight; + } else { + clipTileContext.clearRect(0, 0, tileWidth, tileHeight); + } + clipTileContext.drawImage(tile.getImage(), tileGutter, tileGutter, + tileWidth, tileHeight, 0, 0, tileWidth, tileHeight); + gl.texImage2D(goog.webgl.TEXTURE_2D, 0, + goog.webgl.RGBA, goog.webgl.RGBA, + goog.webgl.UNSIGNED_BYTE, clipTileCanvas); + } else { + gl.texImage2D(goog.webgl.TEXTURE_2D, 0, + goog.webgl.RGBA, goog.webgl.RGBA, + goog.webgl.UNSIGNED_BYTE, tile.getImage()); + } gl.texParameteri( goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_MAG_FILTER, magFilter); gl.texParameteri( diff --git a/src/ol/renderer/webgl/webgltilelayerrenderer.js b/src/ol/renderer/webgl/webgltilelayerrenderer.js index 63e336e0d5..31a25952fb 100644 --- a/src/ol/renderer/webgl/webgltilelayerrenderer.js +++ b/src/ol/renderer/webgl/webgltilelayerrenderer.js @@ -130,6 +130,10 @@ ol.renderer.webgl.TileLayer.prototype.renderFrame = } var z = tileGrid.getZForResolution(view2DState.resolution); var tileResolution = tileGrid.getResolution(z); + + var tileSize = tileGrid.getTileSize(z); + var tileGutter = tileSource.getGutter(); + var center = view2DState.center; var extent; if (tileResolution == view2DState.resolution) { @@ -150,7 +154,6 @@ ol.renderer.webgl.TileLayer.prototype.renderFrame = } else { var tileRangeSize = tileRange.getSize(); - var tileSize = tileGrid.getTileSize(z); var maxDimension = Math.max( tileRangeSize[0] * tileSize[0], @@ -256,7 +259,9 @@ ol.renderer.webgl.TileLayer.prototype.renderFrame = framebufferExtentDimension - 1; goog.vec.Vec4.setFromValues(u_tileOffset, sx, sy, tx, ty); gl.uniform4fv(this.locations_.u_tileOffset, u_tileOffset); - mapRenderer.bindTileTexture(tile, goog.webgl.LINEAR, goog.webgl.LINEAR); + mapRenderer.bindTileTexture(tile, + tileSize[0], tileSize[1], tileGutter, + goog.webgl.LINEAR, goog.webgl.LINEAR); gl.drawArrays(goog.webgl.TRIANGLE_STRIP, 0, 4); } } @@ -289,7 +294,8 @@ ol.renderer.webgl.TileLayer.prototype.renderFrame = tileTextureQueue.enqueue([ tile, tileGrid.getTileCoordCenter(tile.tileCoord), - tileGrid.getResolution(tile.tileCoord.z) + tileGrid.getResolution(tile.tileCoord.z), + tileSize[0], tileSize[1], tileGutter ]); } }, this); diff --git a/src/ol/source/tilesource.js b/src/ol/source/tilesource.js index e40c93d937..541659d2d2 100644 --- a/src/ol/source/tilesource.js +++ b/src/ol/source/tilesource.js @@ -106,6 +106,14 @@ ol.source.Tile.prototype.findLoadedTiles = function(loadedTilesByZ, }; +/** + * @return {number} Gutter. + */ +ol.source.Tile.prototype.getGutter = function() { + return 0; +}; + + /** * @param {number} z Z. * @param {number} x X. diff --git a/src/ol/source/tilewmssource.js b/src/ol/source/tilewmssource.js index d3bbd7c8ce..7f5e849cbb 100644 --- a/src/ol/source/tilewmssource.js +++ b/src/ol/source/tilewmssource.js @@ -35,6 +35,12 @@ ol.source.TileWMS = function(options) { urls = ol.TileUrlFunction.expandUrl(options.url); } + /** + * @private + * @type {number} + */ + this.gutter_ = goog.isDef(options.gutter) ? options.gutter : 0; + /** * @private * @type {Object} @@ -52,7 +58,7 @@ ol.source.TileWMS = function(options) { var tileUrlFunctions = goog.array.map( urls, function(url) { return ol.TileUrlFunction.createFromParamsFunction( - url, this.params_, ol.source.wms.getUrl); + url, this.params_, this.gutter_, ol.source.wms.getUrl); }, this); tileUrlFunction = ol.TileUrlFunction.createFromTileUrlFunctions( tileUrlFunctions); @@ -117,6 +123,14 @@ ol.source.TileWMS = function(options) { goog.inherits(ol.source.TileWMS, ol.source.TileImage); +/** + * @inheritDoc + */ +ol.source.TileWMS.prototype.getGutter = function() { + return this.gutter_; +}; + + /** * @inheritDoc */ diff --git a/src/ol/tileurlfunction.js b/src/ol/tileurlfunction.js index c883fbfe16..008e609f39 100644 --- a/src/ol/tileurlfunction.js +++ b/src/ol/tileurlfunction.js @@ -84,14 +84,16 @@ ol.TileUrlFunction.createFromTileUrlFunctions = function(tileUrlFunctions) { /** * @param {string} baseUrl Base URL (may have query data). - * @param {Object.} params to encode in the URL. + * @param {Object.} params Params to encode in the URL. + * @param {number} gutter Gutter value. * @param {function(this: ol.source.TileImage, string, Object., * ol.Extent, ol.Size, ol.proj.Projection)} paramsFunction params function. * @return {ol.TileUrlFunctionType} Tile URL function. */ ol.TileUrlFunction.createFromParamsFunction = - function(baseUrl, params, paramsFunction) { + function(baseUrl, params, gutter, paramsFunction) { var tmpExtent = ol.extent.createEmpty(); + var tmpSize = [0, 0]; return ( /** * @this {ol.source.TileImage} @@ -107,10 +109,14 @@ ol.TileUrlFunction.createFromParamsFunction = if (goog.isNull(tileGrid)) { tileGrid = ol.tilegrid.getForProjection(projection); } - var size = tileGrid.getTileSize(tileCoord.z); + 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, size, projection); + extent, tmpSize, projection); } }); }; diff --git a/test/spec/ol/tileurlfunction.test.js b/test/spec/ol/tileurlfunction.test.js index 68764b4cb6..6c0e5bcbdd 100644 --- a/test/spec/ol/tileurlfunction.test.js +++ b/test/spec/ol/tileurlfunction.test.js @@ -84,7 +84,7 @@ describe('ol.TileUrlFunction', function() { var fakeTileSource = {getTileGrid: function() {return null;}}; var params = {foo: 'bar'}; var tileUrlFunction = ol.TileUrlFunction.createFromParamsFunction( - 'url', params, paramsFunction); + 'url', params, 0, paramsFunction); it('calls the passed function with the correct arguments', function() { var args = tileUrlFunction.call(fakeTileSource, new ol.TileCoord(0, 0, 0), projection);