From c1b16217f28f50d4585a2ffda603b83042cbd2cc Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Mon, 14 Dec 2015 09:41:58 +0100 Subject: [PATCH] Conditionally render tiles to a separate tile canvas Because clip geometries are anti-aliased in most browsers, there will be tiny gaps between tiles. If tiles are rendered to a tile canvas which is then drawn to the map canvas upon composition, these gaps can be avoided. For rotated views, it is stil necessary to clip the tile, but in this case a 1-pixel buffer is used. This change also brings a huge performance improvement for panning, because the fully rendered tiles can be reused. Because of the added cost of using drawImage in addition to replaying the tile replay group, we fall back to directly drawing to the map canvas when the tile canvas would be too large, or during interaction/animation when resolution or rotation change. --- src/ol/render/canvas/canvasreplay.js | 43 +++--- .../canvas/canvasvectortilelayerrenderer.js | 122 ++++++++++++++---- src/ol/vectortile.js | 15 +++ 3 files changed, 136 insertions(+), 44 deletions(-) diff --git a/src/ol/render/canvas/canvasreplay.js b/src/ol/render/canvas/canvasreplay.js index 6c6668ca75..c53ca842f8 100644 --- a/src/ol/render/canvas/canvasreplay.js +++ b/src/ol/render/canvas/canvasreplay.js @@ -1995,32 +1995,35 @@ ol.render.canvas.ReplayGroup.prototype.isEmpty = function() { * @param {number} viewRotation View rotation. * @param {Object.} skippedFeaturesHash Ids of features * to skip. + * @param {boolean=} opt_clip Clip at `maxExtent`. Default is true. */ -ol.render.canvas.ReplayGroup.prototype.replay = function( - context, pixelRatio, transform, viewRotation, skippedFeaturesHash) { +ol.render.canvas.ReplayGroup.prototype.replay = function(context, pixelRatio, + transform, viewRotation, skippedFeaturesHash, opt_clip) { /** @type {Array.} */ var zs = Object.keys(this.replaysByZIndex_).map(Number); zs.sort(ol.array.numberSafeCompareFunction); - // setup clipping so that the parts of over-simplified geometries are not - // visible outside the current extent when panning - var maxExtent = this.maxExtent_; - var minX = maxExtent[0]; - var minY = maxExtent[1]; - var maxX = maxExtent[2]; - var maxY = maxExtent[3]; - var flatClipCoords = [minX, minY, minX, maxY, maxX, maxY, maxX, minY]; - ol.geom.flat.transform.transform2D( - flatClipCoords, 0, 8, 2, transform, flatClipCoords); - context.save(); - context.beginPath(); - context.moveTo(flatClipCoords[0], flatClipCoords[1]); - context.lineTo(flatClipCoords[2], flatClipCoords[3]); - context.lineTo(flatClipCoords[4], flatClipCoords[5]); - context.lineTo(flatClipCoords[6], flatClipCoords[7]); - context.closePath(); - context.clip(); + if (opt_clip !== false) { + // setup clipping so that the parts of over-simplified geometries are not + // visible outside the current extent when panning + var maxExtent = this.maxExtent_; + var minX = maxExtent[0]; + var minY = maxExtent[1]; + var maxX = maxExtent[2]; + var maxY = maxExtent[3]; + var flatClipCoords = [minX, minY, minX, maxY, maxX, maxY, maxX, minY]; + ol.geom.flat.transform.transform2D( + flatClipCoords, 0, 8, 2, transform, flatClipCoords); + context.save(); + context.beginPath(); + context.moveTo(flatClipCoords[0], flatClipCoords[1]); + context.lineTo(flatClipCoords[2], flatClipCoords[3]); + context.lineTo(flatClipCoords[4], flatClipCoords[5]); + context.lineTo(flatClipCoords[6], flatClipCoords[7]); + context.closePath(); + context.clip(); + } var i, ii, j, jj, replays, replay; for (i = 0, ii = zs.length; i < ii; ++i) { diff --git a/src/ol/renderer/canvas/canvasvectortilelayerrenderer.js b/src/ol/renderer/canvas/canvasvectortilelayerrenderer.js index b299254f80..fe7109d3cc 100644 --- a/src/ol/renderer/canvas/canvasvectortilelayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectortilelayerrenderer.js @@ -11,6 +11,7 @@ goog.require('ol.ViewHint'); goog.require('ol.array'); goog.require('ol.dom'); goog.require('ol.extent'); +goog.require('ol.geom.flat.transform'); goog.require('ol.layer.VectorTile'); goog.require('ol.proj.Units'); goog.require('ol.render.EventType'); @@ -51,6 +52,18 @@ ol.renderer.canvas.VectorTileLayer = function(layer) { */ this.renderedTiles_ = []; + /** + * @private + * @type {number} + */ + this.resolution_ = NaN; + + /** + * @private + * @type {number} + */ + this.rotation_ = NaN; + /** * @private * @type {ol.Extent} @@ -110,44 +123,101 @@ ol.renderer.canvas.VectorTileLayer.prototype.composeFrame = // see http://jsperf.com/context-save-restore-versus-variable var alpha = replayContext.globalAlpha; replayContext.globalAlpha = layerState.opacity; + var imageSmoothingEnabled = replayContext.imageSmoothingEnabled; + replayContext.imageSmoothingEnabled = false; + var tilesToDraw = this.renderedTiles_; var tileGrid = source.getTileGrid(); - var currentZ, i, ii, origin, tile, tileSize; - var tilePixelRatio, tilePixelResolution, tilePixelSize, tileResolution; + var currentZ, height, i, ii, insertPoint, insertTransform, origin, pixelScale; + var pixelSpace, replayState, rotatedTileExtent, rotatedTileSize, size, tile; + var tileCenter, tileContext, tileExtent, tilePixelResolution, tilePixelSize; + var tileResolution, tileSize, tileTransform, width; for (i = 0, ii = tilesToDraw.length; i < ii; ++i) { tile = tilesToDraw[i]; + replayState = tile.getReplayState(); + tileExtent = tileGrid.getTileCoordExtent( + tile.getTileCoord(), this.tmpExtent_); currentZ = tile.getTileCoord()[0]; - tileSize = tileGrid.getTileSize(currentZ); - tilePixelSize = source.getTilePixelSize(currentZ, pixelRatio, projection); - tilePixelRatio = tilePixelSize[0] / - ol.size.toSize(tileSize, this.tmpSize_)[0]; + tileSize = ol.size.toSize(tileGrid.getTileSize(currentZ), this.tmpSize_); + pixelSpace = tile.getProjection().getUnits() == ol.proj.Units.TILE_PIXELS; + size = frameState.size; tileResolution = tileGrid.getResolution(currentZ); - tilePixelResolution = tileResolution / tilePixelRatio; - if (tile.getProjection().getUnits() == ol.proj.Units.TILE_PIXELS) { - origin = ol.extent.getTopLeft(tileGrid.getTileCoordExtent( - tile.getTileCoord(), this.tmpExtent_)); - transform = ol.vec.Mat4.makeTransform2D(this.tmpTransform_, - pixelRatio * frameState.size[0] / 2, - pixelRatio * frameState.size[1] / 2, - pixelRatio * tilePixelResolution / resolution, - pixelRatio * tilePixelResolution / resolution, - viewState.rotation, - (origin[0] - center[0]) / tilePixelResolution, - (center[1] - origin[1]) / tilePixelResolution); + tilePixelResolution = tileResolution / source.getTilePixelRatio(); + pixelScale = pixelRatio / resolution; + scale = tileResolution / resolution; + offsetX = Math.round(pixelRatio * size[0] / 2); + offsetY = Math.round(pixelRatio * size[1] / 2); + width = tileSize[0] * pixelRatio * scale; + height = tileSize[1] * pixelRatio * scale; + if (width < 1 || width > size[0]) { + if (pixelSpace) { + origin = ol.extent.getTopLeft(tileExtent); + tileTransform = ol.vec.Mat4.makeTransform2D(this.tmpTransform_, + pixelRatio * size[0] / 2, pixelRatio * size[1] / 2, + pixelScale * tilePixelResolution, + pixelScale * tilePixelResolution, + rotation, + (origin[0] - center[0]) / tilePixelResolution, + (center[1] - origin[1]) / tilePixelResolution); + } else { + tileTransform = transform; + } + replayState.replayGroup.replay(replayContext, pixelRatio, + tileTransform, rotation, skippedFeatureUids); + } else { + rotatedTileExtent = ol.extent.getForViewAndSize( + ol.extent.getCenter(tileExtent), tileResolution, rotation, tileSize); + rotatedTileSize = [ol.extent.getWidth(rotatedTileExtent), + ol.extent.getHeight(rotatedTileExtent)]; + tilePixelSize = source.getTilePixelSize(currentZ, pixelRatio, projection); + if (pixelSpace) { + tileTransform = ol.vec.Mat4.makeTransform2D(this.tmpTransform_, + width / 2, height / 2, + pixelScale * tilePixelResolution, pixelScale * tilePixelResolution, + rotation, + -tilePixelSize[0] / 2, -tilePixelSize[1] / 2); + } else { + tileCenter = ol.extent.getCenter(rotatedTileExtent); + tileTransform = ol.vec.Mat4.makeTransform2D(this.tmpTransform_, + width / 2, height / 2, + pixelScale, -pixelScale, + -rotation, + -tileCenter[0], -tileCenter[1]); + } + tileContext = tile.getContext(); + if (replayState.resolution !== resolution || + replayState.rotation !== rotation) { + replayState.resolution = resolution; + replayState.rotation = rotation; + tileContext.canvas.width = width + 0.5; + tileContext.canvas.height = height + 0.5; + replayState.replayGroup.replay(tileContext, pixelRatio, + tileTransform, rotation, skippedFeatureUids, rotation !== 0); + } + insertTransform = ol.vec.Mat4.makeTransform2D(this.tmpTransform_, + (pixelRatio * size[0] - width) / 2, + (pixelRatio * size[1] - height) / 2, + pixelScale, -pixelScale, + -rotation, + -center[0], -center[1]); + insertPoint = ol.geom.flat.transform.transform2D( + ol.extent.getCenter(rotatedTileExtent), 0, 1, 2, insertTransform); + replayContext.drawImage(tileContext.canvas, + insertPoint[0], insertPoint[1]); } - tile.getReplayState().replayGroup.replay(replayContext, pixelRatio, - transform, rotation, skippedFeatureUids); } - transform = this.getTransform(frameState, 0); + this.resolution_ = resolution; + this.rotation_ = rotation; if (replayContext != context) { this.dispatchRenderEvent(replayContext, frameState, transform); context.drawImage(replayContext.canvas, 0, 0); } replayContext.globalAlpha = alpha; + replayContext.imageSmoothingEnabled = imageSmoothingEnabled; this.dispatchPostComposeEvent(context, frameState, transform); }; @@ -179,16 +249,19 @@ ol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup = function(tile, 'Source is an ol.source.VectorTile'); var tileGrid = source.getTileGrid(); var tileCoord = tile.getTileCoord(); + var buffer = 1; + var resolution = tileGrid.getResolution(tileCoord[0]); var pixelSpace = tile.getProjection().getUnits() == ol.proj.Units.TILE_PIXELS; var extent; if (pixelSpace) { var tilePixelSize = source.getTilePixelSize(tileCoord[0], pixelRatio, tile.getProjection()); - extent = [0, 0, tilePixelSize[0], tilePixelSize[1]]; + extent = [-buffer, -buffer, + tilePixelSize[0] + buffer, tilePixelSize[1] + buffer]; } else { - extent = tileGrid.getTileCoordExtent(tileCoord); + extent = ol.extent.buffer(tileGrid.getTileCoordExtent(tileCoord), + buffer * resolution); } - var resolution = tileGrid.getResolution(tileCoord[0]); var tileResolution = pixelSpace ? source.getTilePixelRatio() : resolution; replayState.dirty = false; var replayGroup = new ol.render.canvas.ReplayGroup(0, extent, @@ -234,6 +307,7 @@ ol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup = function(tile, replayState.renderedRevision = revision; replayState.renderedRenderOrder = renderOrder; replayState.replayGroup = replayGroup; + replayState.resolution = NaN; }; diff --git a/src/ol/vectortile.js b/src/ol/vectortile.js index 5a5e1efe2c..55e22eb4cb 100644 --- a/src/ol/vectortile.js +++ b/src/ol/vectortile.js @@ -4,6 +4,7 @@ goog.require('ol.Tile'); goog.require('ol.TileCoord'); goog.require('ol.TileLoadFunctionType'); goog.require('ol.TileState'); +goog.require('ol.dom'); goog.require('ol.proj.Projection'); @@ -33,6 +34,12 @@ ol.VectorTile = goog.base(this, tileCoord, state); + /** + * @private + * @type {CanvasRenderingContext2D} + */ + this.context_ = ol.dom.createCanvasContext2D(); + /** * @private * @type {ol.format.Feature} @@ -84,6 +91,14 @@ ol.VectorTile = goog.inherits(ol.VectorTile, ol.Tile); +/** + * @return {CanvasRenderingContext2D} + */ +ol.VectorTile.prototype.getContext = function() { + return this.context_; +}; + + /** * @inheritDoc */