From ccdb955cd9b253ae15306fecc1a2e6e6efd72722 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 4 Nov 2016 17:08:28 +0100 Subject: [PATCH 1/5] Simplify canvas tile renderer with image composition --- src/ol/renderer/canvas/imagelayer.js | 4 +- src/ol/renderer/canvas/tilelayer.js | 318 ++++++++++----------------- 2 files changed, 119 insertions(+), 203 deletions(-) diff --git a/src/ol/renderer/canvas/imagelayer.js b/src/ol/renderer/canvas/imagelayer.js index 828b902e80..1b2111f537 100644 --- a/src/ol/renderer/canvas/imagelayer.js +++ b/src/ol/renderer/canvas/imagelayer.js @@ -34,9 +34,9 @@ ol.renderer.canvas.ImageLayer = function(imageLayer) { /** * @private - * @type {?ol.Transform} + * @type {ol.Transform} */ - this.imageTransformInv_ = null; + this.coordinateToCanvasPixelTransform_ = ol.transform.create(); /** * @private diff --git a/src/ol/renderer/canvas/tilelayer.js b/src/ol/renderer/canvas/tilelayer.js index a83f80ab38..bf502c2bc6 100644 --- a/src/ol/renderer/canvas/tilelayer.js +++ b/src/ol/renderer/canvas/tilelayer.js @@ -9,8 +9,6 @@ goog.require('ol.Tile'); goog.require('ol.array'); goog.require('ol.dom'); goog.require('ol.extent'); -goog.require('ol.render.canvas'); -goog.require('ol.render.Event'); goog.require('ol.renderer.canvas.Layer'); @@ -30,10 +28,28 @@ ol.renderer.canvas.TileLayer = function(tileLayer) { this.context = ol.dom.createCanvasContext2D(); /** - * @protected - * @type {!Array.} + * @private + * @type {ol.Extent} */ - this.renderedTiles = []; + this.renderedExtent_ = ol.extent.createEmpty(); + + /** + * @private + * @type {number} + */ + this.renderedResolution_; + + /** + * @private + * @type {number} + */ + this.renderedRevision_; + + /** + * @private + * @type {Array.} + */ + this.renderedTileKeys_ = null; /** * @protected @@ -53,6 +69,12 @@ ol.renderer.canvas.TileLayer = function(tileLayer) { */ this.imageTransform_ = ol.transform.create(); + /** + * @private + * @type {ol.Transform} + */ + this.coordinateToCanvasPixelTransform_ = ol.transform.create(); + /** * @protected * @type {number} @@ -63,18 +85,6 @@ ol.renderer.canvas.TileLayer = function(tileLayer) { ol.inherits(ol.renderer.canvas.TileLayer, ol.renderer.canvas.Layer); -/** - * @inheritDoc - */ -ol.renderer.canvas.TileLayer.prototype.composeFrame = function( - frameState, layerState, context) { - var transform = this.getTransform(frameState, 0); - this.dispatchPreComposeEvent(context, frameState, transform); - this.renderTileImages(context, frameState, layerState); - this.dispatchPostComposeEvent(context, frameState, transform); -}; - - /** * @inheritDoc */ @@ -82,13 +92,17 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function( frameState, layerState) { var pixelRatio = frameState.pixelRatio; + var size = frameState.size; var viewState = frameState.viewState; var projection = viewState.projection; + var viewResolution = viewState.resolution; + var viewCenter = viewState.center; var tileLayer = this.getLayer(); var tileSource = /** @type {ol.source.Tile} */ (tileLayer.getSource()); + var sourceRevision = tileSource.getRevision(); var tileGrid = tileSource.getTileGridForProjection(projection); - var z = tileGrid.getZForResolution(viewState.resolution, this.zDirection); + var z = tileGrid.getZForResolution(viewResolution, this.zDirection); var tileResolution = tileGrid.getResolution(z); var extent = frameState.extent; @@ -102,12 +116,29 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function( var tileRange = tileGrid.getTileRangeForExtentAndResolution( extent, tileResolution); + var imageExtent = tileGrid.getTileRangeExtent(z, tileRange); + + var tilePixelRatio = tileSource.getTilePixelRatio(pixelRatio); + var scale = pixelRatio / tilePixelRatio * tileResolution / viewResolution; + + var transform = ol.transform.compose(ol.transform.reset(this.imageTransform_), + pixelRatio * size[0] / 2, pixelRatio * size[1] / 2, + scale, scale, + 0, + tilePixelRatio * (imageExtent[0] - viewCenter[0]) / tileResolution, + tilePixelRatio * (viewCenter[1] - imageExtent[3]) / tileResolution); + ol.transform.compose(ol.transform.reset(this.coordinateToCanvasPixelTransform_), + pixelRatio * size[0] / 2 - transform[4], pixelRatio * size[1] / 2 - transform[5], + pixelRatio / viewResolution, -pixelRatio / viewResolution, + 0, + -viewCenter[0], -viewCenter[1]); /** * @type {Object.>} */ var tilesToDrawByZ = {}; tilesToDrawByZ[z] = {}; + var loadedTileKeys = []; var findLoadedTiles = this.createLoadedTileFinder( tileSource, projection, tilesToDrawByZ); @@ -116,7 +147,7 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function( var tmpExtent = this.tmpExtent; var tmpTileRange = new ol.TileRange(0, 0, 0, 0); - var childTileRange, fullyLoaded, tile, x, y; + var childTileRange, fullyLoaded, tile, tileCoordKey, x, y; var drawableTile = ( /** * @param {!ol.Tile} tile Tile. @@ -135,7 +166,11 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function( tile = tile.getInterimTile(); } if (drawableTile(tile)) { - tilesToDrawByZ[z][tile.tileCoord.toString()] = tile; + if (tile.getState() == ol.Tile.State.LOADED) { + loadedTileKeys.push(tileCoordKey); + tileCoordKey = tile.tileCoord.toString(); + tilesToDrawByZ[z][tileCoordKey] = tile; + } continue; } fullyLoaded = tileGrid.forEachTileCoordParentTileRange( @@ -150,22 +185,52 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function( } } + loadedTileKeys.sort(); - /** @type {Array.} */ - var zs = Object.keys(tilesToDrawByZ).map(Number); - zs.sort(ol.array.numberSafeCompareFunction); - var renderables = this.renderedTiles; - renderables.length = 0; - var i, ii, currentZ, tileCoordKey, tilesToDraw; - for (i = 0, ii = zs.length; i < ii; ++i) { - currentZ = zs[i]; - tilesToDraw = tilesToDrawByZ[currentZ]; - for (tileCoordKey in tilesToDraw) { - tile = tilesToDraw[tileCoordKey]; - if (tile.getState() == ol.Tile.State.LOADED) { - renderables.push(tile); + if (tileResolution != this.renderedResolution_ || + sourceRevision != this.renderedRevision_ || + !ol.extent.equals(this.renderedExtent_, imageExtent) || + !ol.array.equals(loadedTileKeys, this.renderedTileKeys_)) { + + var tilePixelSize = tileSource.getTilePixelSize(z, pixelRatio, projection); + var width = tileRange.getWidth() * tilePixelSize[0]; + var height = tileRange.getHeight() * tilePixelSize[0]; + var context = this.context; + var canvas = context.canvas; + var opaque = tileSource.getOpaque(projection); + if (canvas.width != width || canvas.height != height) { + canvas.width = width; + canvas.height = height; + } + + /** @type {Array.} */ + var zs = Object.keys(tilesToDrawByZ).map(Number); + zs.sort(ol.array.numberSafeCompareFunction); + var currentResolution, currentScale, currentTilePixelSize, currentZ, i, ii; + var image, tileExtent, tileGutter, tilesToDraw, w, h; + for (i = 0, ii = zs.length; i < ii; ++i) { + currentZ = zs[i]; + currentTilePixelSize = tileSource.getTilePixelSize(currentZ, pixelRatio, projection); + currentResolution = tileGrid.getResolution(currentZ); + currentScale = currentResolution / tileResolution; + tileGutter = tilePixelRatio * tileSource.getGutter(projection); + tilesToDraw = tilesToDrawByZ[currentZ]; + for (tileCoordKey in tilesToDraw) { + tile = tilesToDraw[tileCoordKey]; + tileExtent = tileGrid.getTileCoordExtent(tile.getTileCoord(), tmpExtent); + x = (tileExtent[0] - imageExtent[0]) / tileResolution * tilePixelRatio; + y = (imageExtent[3] - tileExtent[3]) / tileResolution * tilePixelRatio; + w = currentTilePixelSize[0] * currentScale; + h = currentTilePixelSize[1] * currentScale; + if (!opaque) { + context.clearRect(x, y, w, h); + } + image = tile.getImage(); + context.drawImage(image, tileGutter, tileGutter, + image.width, image.height, x, y, w, h); } } + } this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange); @@ -174,6 +239,11 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function( this.scheduleExpireCache(frameState, tileSource); this.updateLogos(frameState, tileSource); + this.renderedTileKeys_ = loadedTileKeys; + this.renderedExtent_ = imageExtent; + this.renderedResolution_ = tileResolution; + this.renderedRevision_ = sourceRevision; + return true; }; @@ -189,186 +259,32 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function( */ ol.renderer.canvas.TileLayer.prototype.forEachLayerAtPixel = function( pixel, frameState, callback, thisArg) { - var canvas = this.context.canvas; - var size = frameState.size; - var pixelRatio = frameState.pixelRatio; - canvas.width = size[0] * pixelRatio; - canvas.height = size[1] * pixelRatio; - this.composeFrame(frameState, this.getLayer().getLayerState(), this.context); - - var imageData = this.context.getImageData( - pixel[0], pixel[1], 1, 1).data; + var coordinate = ol.transform.apply(frameState.pixelToCoordinateTransform, pixel.slice()); + var canvasPixel = ol.transform.apply(this.coordinateToCanvasPixelTransform_, coordinate); + var imageData = this.context.getImageData(canvasPixel[0], canvasPixel[1], 1, 1).data; if (imageData[3] > 0) { - return callback.call(thisArg, this.getLayer(), imageData); + return callback.call(thisArg, this.getLayer(), imageData); } else { return undefined; } + }; /** - * @param {CanvasRenderingContext2D} context Context. - * @param {olx.FrameState} frameState Frame state. - * @param {ol.LayerState} layerState Layer state. - * @protected + * @inheritDoc */ -ol.renderer.canvas.TileLayer.prototype.renderTileImages = function(context, frameState, layerState) { - var tilesToDraw = this.renderedTiles; - if (tilesToDraw.length === 0) { - return; - } +ol.renderer.canvas.TileLayer.prototype.getImage = function() { + return this.context.canvas; +}; - var pixelRatio = frameState.pixelRatio; - var viewState = frameState.viewState; - var center = viewState.center; - var projection = viewState.projection; - var resolution = viewState.resolution; - var rotation = viewState.rotation; - var size = frameState.size; - var offsetX = Math.round(pixelRatio * size[0] / 2); - var offsetY = Math.round(pixelRatio * size[1] / 2); - var pixelScale = pixelRatio / resolution; - var layer = this.getLayer(); - var source = /** @type {ol.source.Tile} */ (layer.getSource()); - var tileGutter = source.getTilePixelRatio(pixelRatio) * source.getGutter(projection); - var tileGrid = source.getTileGridForProjection(projection); - var hasRenderListeners = layer.hasListener(ol.render.Event.Type.RENDER); - var renderContext = context; - var drawScale = 1; - var drawOffsetX, drawOffsetY, drawSize; - if (rotation || hasRenderListeners) { - renderContext = this.context; - var renderCanvas = renderContext.canvas; - drawScale = source.getTilePixelRatio(pixelRatio) / pixelRatio; - var width = context.canvas.width * drawScale; - var height = context.canvas.height * drawScale; - // Make sure the canvas is big enough for all possible rotation angles - drawSize = Math.round(Math.sqrt(width * width + height * height)); - if (renderCanvas.width != drawSize) { - renderCanvas.width = renderCanvas.height = drawSize; - } else { - renderContext.clearRect(0, 0, drawSize, drawSize); - } - drawOffsetX = (drawSize - width) / 2 / drawScale; - drawOffsetY = (drawSize - height) / 2 / drawScale; - pixelScale *= drawScale; - offsetX = Math.round(drawScale * (offsetX + drawOffsetX)); - offsetY = Math.round(drawScale * (offsetY + drawOffsetY)); - } - // for performance reasons, context.save / context.restore is not used - // to save and restore the transformation matrix and the opacity. - // see http://jsperf.com/context-save-restore-versus-variable - var alpha = renderContext.globalAlpha; - renderContext.globalAlpha = layerState.opacity; - - var pixelExtents; - var opaque = source.getOpaque(projection) && layerState.opacity == 1; - if (!opaque) { - tilesToDraw.reverse(); - pixelExtents = []; - } - - var extent = layerState.extent; - var clipped = extent !== undefined; - if (clipped) { - var topLeft = ol.extent.getTopLeft(/** @type {ol.Extent} */ (extent)); - var topRight = ol.extent.getTopRight(/** @type {ol.Extent} */ (extent)); - var bottomRight = ol.extent.getBottomRight(/** @type {ol.Extent} */ (extent)); - var bottomLeft = ol.extent.getBottomLeft(/** @type {ol.Extent} */ (extent)); - - ol.transform.apply(frameState.coordinateToPixelTransform, topLeft); - ol.transform.apply(frameState.coordinateToPixelTransform, topRight); - ol.transform.apply(frameState.coordinateToPixelTransform, bottomRight); - ol.transform.apply(frameState.coordinateToPixelTransform, bottomLeft); - - var ox = drawOffsetX || 0; - var oy = drawOffsetY || 0; - renderContext.save(); - var cx = (renderContext.canvas.width) / 2; - var cy = (renderContext.canvas.height) / 2; - ol.render.canvas.rotateAtOffset(renderContext, -rotation, cx, cy); - renderContext.beginPath(); - renderContext.moveTo(drawScale * (topLeft[0] * pixelRatio + ox), - drawScale * (topLeft[1] * pixelRatio + oy)); - renderContext.lineTo(drawScale * (topRight[0] * pixelRatio + ox), - drawScale * (topRight[1] * pixelRatio + oy)); - renderContext.lineTo(drawScale * (bottomRight[0] * pixelRatio + ox), - drawScale * (bottomRight[1] * pixelRatio + oy)); - renderContext.lineTo(drawScale * (bottomLeft[0] * pixelRatio + ox), - drawScale * (bottomLeft[1] * pixelRatio + oy)); - renderContext.clip(); - ol.render.canvas.rotateAtOffset(renderContext, rotation, cx, cy); - } - - for (var i = 0, ii = tilesToDraw.length; i < ii; ++i) { - var tile = tilesToDraw[i]; - var tileCoord = tile.getTileCoord(); - var tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent); - var currentZ = tileCoord[0]; - // Calculate all insert points by tile widths from a common origin to avoid - // gaps caused by rounding - var origin = ol.extent.getBottomLeft(tileGrid.getTileCoordExtent( - tileGrid.getTileCoordForCoordAndZ(center, currentZ, this.tmpTileCoord_))); - var w = Math.round(ol.extent.getWidth(tileExtent) * pixelScale); - var h = Math.round(ol.extent.getHeight(tileExtent) * pixelScale); - var left = Math.round((tileExtent[0] - origin[0]) * pixelScale / w) * w + - offsetX + Math.round((origin[0] - center[0]) * pixelScale); - var top = Math.round((origin[1] - tileExtent[3]) * pixelScale / h) * h + - offsetY + Math.round((center[1] - origin[1]) * pixelScale); - if (!opaque) { - var pixelExtent = [left, top, left + w, top + h]; - // Create a clip mask for regions in this low resolution tile that are - // already filled by a higher resolution tile - renderContext.save(); - for (var j = 0, jj = pixelExtents.length; j < jj; ++j) { - var clipExtent = pixelExtents[j]; - if (ol.extent.intersects(pixelExtent, clipExtent)) { - renderContext.beginPath(); - // counter-clockwise (outer ring) for current tile - renderContext.moveTo(pixelExtent[0], pixelExtent[1]); - renderContext.lineTo(pixelExtent[0], pixelExtent[3]); - renderContext.lineTo(pixelExtent[2], pixelExtent[3]); - renderContext.lineTo(pixelExtent[2], pixelExtent[1]); - // clockwise (inner ring) for higher resolution tile - renderContext.moveTo(clipExtent[0], clipExtent[1]); - renderContext.lineTo(clipExtent[2], clipExtent[1]); - renderContext.lineTo(clipExtent[2], clipExtent[3]); - renderContext.lineTo(clipExtent[0], clipExtent[3]); - renderContext.closePath(); - renderContext.clip(); - } - } - pixelExtents.push(pixelExtent); - } - var tilePixelSize = source.getTilePixelSize(currentZ, pixelRatio, projection); - renderContext.drawImage(tile.getImage(), tileGutter, tileGutter, - tilePixelSize[0], tilePixelSize[1], left, top, w, h); - if (!opaque) { - renderContext.restore(); - } - } - - if (clipped) { - renderContext.restore(); - } - - if (hasRenderListeners) { - var dX = drawOffsetX - offsetX / drawScale + offsetX; - var dY = drawOffsetY - offsetY / drawScale + offsetY; - var imageTransform = ol.transform.compose(this.imageTransform_, - drawSize / 2 - dX, drawSize / 2 - dY, - pixelScale, -pixelScale, - -rotation, - -center[0] + dX / pixelScale, -center[1] - dY / pixelScale); - this.dispatchRenderEvent(renderContext, frameState, imageTransform); - } - if (rotation || hasRenderListeners) { - context.drawImage(renderContext.canvas, -Math.round(drawOffsetX), - -Math.round(drawOffsetY), drawSize / drawScale, drawSize / drawScale); - } - renderContext.globalAlpha = alpha; +/** + * @inheritDoc + */ +ol.renderer.canvas.TileLayer.prototype.getImageTransform = function() { + return this.imageTransform_; }; From 2aa4f0c01c92435bc1645bb105dd040fbbfe127a Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 4 Nov 2016 17:10:40 +0100 Subject: [PATCH 2/5] Fix forEachLayerAtPixel for the canvas renderer --- src/ol/renderer/canvas/imagelayer.js | 22 +++++++++---------- src/ol/renderer/canvas/layer.js | 24 ++++++++++++++------ src/ol/renderer/canvas/map.js | 33 ++++++++++++++++++++++++++++ src/ol/renderer/canvas/tilelayer.js | 7 +++--- src/ol/renderer/layer.js | 24 -------------------- src/ol/renderer/map.js | 25 ++------------------- src/ol/renderer/webgl/imagelayer.js | 8 +------ src/ol/renderer/webgl/layer.js | 13 +++++++++++ src/ol/renderer/webgl/map.js | 2 +- src/ol/renderer/webgl/tilelayer.js | 8 +------ src/ol/renderer/webgl/vectorlayer.js | 8 +------ 11 files changed, 82 insertions(+), 92 deletions(-) diff --git a/src/ol/renderer/canvas/imagelayer.js b/src/ol/renderer/canvas/imagelayer.js index 1b2111f537..6dbf8d4abd 100644 --- a/src/ol/renderer/canvas/imagelayer.js +++ b/src/ol/renderer/canvas/imagelayer.js @@ -70,7 +70,7 @@ ol.renderer.canvas.ImageLayer.prototype.forEachFeatureAtCoordinate = function(co /** - * @param {ol.Pixel} pixel Pixel. + * @param {ol.Coordinate} coordinate Coordinate. * @param {olx.FrameState} frameState FrameState. * @param {function(this: S, ol.layer.Layer, (Uint8ClampedArray|Uint8Array)): T} callback Layer * callback. @@ -78,7 +78,7 @@ ol.renderer.canvas.ImageLayer.prototype.forEachFeatureAtCoordinate = function(co * @return {T|undefined} Callback result. * @template S,T,U */ -ol.renderer.canvas.ImageLayer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) { +ol.renderer.canvas.ImageLayer.prototype.forEachLayerAtCoordinate = function(coordinate, frameState, callback, thisArg) { if (!this.getImage()) { return undefined; } @@ -86,8 +86,6 @@ ol.renderer.canvas.ImageLayer.prototype.forEachLayerAtPixel = function(pixel, fr if (this.getLayer().getSource() instanceof ol.source.ImageVector) { // for ImageVector sources use the original hit-detection logic, // so that for example also transparent polygons are detected - var coordinate = ol.transform.apply( - frameState.pixelToCoordinateTransform, pixel.slice()); var hasFeature = this.forEachFeatureAtCoordinate( coordinate, frameState, ol.functions.TRUE, this); @@ -97,13 +95,8 @@ ol.renderer.canvas.ImageLayer.prototype.forEachLayerAtPixel = function(pixel, fr return undefined; } } else { - // for all other image sources directly check the image - if (!this.imageTransformInv_) { - this.imageTransformInv_ = ol.transform.invert(this.imageTransform_.slice()); - } - - var pixelOnCanvas = - this.getPixelOnCanvas(pixel, this.imageTransformInv_); + var pixelOnCanvas = ol.transform.apply( + this.coordinateToCanvasPixelTransform_, coordinate.slice()); if (!this.hitCanvasContext_) { this.hitCanvasContext_ = ol.dom.createCanvasContext2D(1, 1); @@ -145,6 +138,7 @@ ol.renderer.canvas.ImageLayer.prototype.getImageTransform = function() { ol.renderer.canvas.ImageLayer.prototype.prepareFrame = function(frameState, layerState) { var pixelRatio = frameState.pixelRatio; + var size = frameState.size; var viewState = frameState.viewState; var viewCenter = viewState.center; var viewResolution = viewState.resolution; @@ -197,7 +191,11 @@ ol.renderer.canvas.ImageLayer.prototype.prepareFrame = function(frameState, laye ol.transform.translate(transform, imagePixelRatio * (imageExtent[0] - viewCenter[0]) / imageResolution, imagePixelRatio * (viewCenter[1] - imageExtent[3]) / imageResolution); - this.imageTransformInv_ = null; + ol.transform.compose(ol.transform.reset(this.coordinateToCanvasPixelTransform_), + pixelRatio * size[0] / 2 - transform[4], pixelRatio * size[1] / 2 - transform[5], + pixelRatio / viewResolution, -pixelRatio / viewResolution, + 0, + -viewCenter[0], -viewCenter[1]); this.updateAttributions(frameState.attributions, image.getAttributions()); this.updateLogos(frameState, imageSource); } diff --git a/src/ol/renderer/canvas/layer.js b/src/ol/renderer/canvas/layer.js index a9df0e7dea..5ffb9e1f93 100644 --- a/src/ol/renderer/canvas/layer.js +++ b/src/ol/renderer/canvas/layer.js @@ -2,6 +2,7 @@ goog.provide('ol.renderer.canvas.Layer'); goog.require('ol'); goog.require('ol.extent'); +goog.require('ol.functions'); goog.require('ol.render.Event'); goog.require('ol.render.canvas'); goog.require('ol.render.canvas.Immediate'); @@ -214,12 +215,21 @@ ol.renderer.canvas.Layer.prototype.prepareFrame = function(frameState, layerStat /** - * @param {ol.Pixel} pixelOnMap Pixel. - * @param {ol.Transform} imageTransformInv The transformation matrix - * to convert from a map pixel to a canvas pixel. - * @return {ol.Pixel} The pixel. - * @protected + * @param {ol.Coordinate} coordinate Coordinate. + * @param {olx.FrameState} frameState Frame state. + * @param {function(this: S, ol.layer.Layer, (Uint8ClampedArray|Uint8Array)): T} callback Layer callback. + * @param {S} thisArg Value to use as `this` when executing `callback`. + * @return {T|undefined} Callback result. + * @template S,T */ -ol.renderer.canvas.Layer.prototype.getPixelOnCanvas = function(pixelOnMap, imageTransformInv) { - return ol.transform.apply(imageTransformInv, pixelOnMap.slice()); +ol.renderer.canvas.Layer.prototype.forEachLayerAtCoordinate = function(coordinate, frameState, callback, thisArg) { + + var hasFeature = this.forEachFeatureAtCoordinate( + coordinate, frameState, ol.functions.TRUE, this); + + if (hasFeature) { + return callback.call(thisArg, this.getLayer(), null); + } else { + return undefined; + } }; diff --git a/src/ol/renderer/canvas/map.js b/src/ol/renderer/canvas/map.js index 6dcb6a10a6..3e78ec20ab 100644 --- a/src/ol/renderer/canvas/map.js +++ b/src/ol/renderer/canvas/map.js @@ -200,3 +200,36 @@ ol.renderer.canvas.Map.prototype.renderFrame = function(frameState) { this.scheduleRemoveUnusedLayerRenderers(frameState); this.scheduleExpireIconCache(frameState); }; + + +/** + * @inheritDoc + */ +ol.renderer.canvas.Map.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg, + layerFilter, thisArg2) { + var result; + var viewState = frameState.viewState; + var viewResolution = viewState.resolution; + + var layerStates = frameState.layerStatesArray; + var numLayers = layerStates.length; + + var coordinate = ol.transform.apply( + frameState.pixelToCoordinateTransform, pixel.slice()); + + var i; + for (i = numLayers - 1; i >= 0; --i) { + var layerState = layerStates[i]; + var layer = layerState.layer; + if (ol.layer.Layer.visibleAtResolution(layerState, viewResolution) && + layerFilter.call(thisArg2, layer)) { + var layerRenderer = /** @type {ol.renderer.canvas.Layer} */ (this.getLayerRenderer(layer)); + result = layerRenderer.forEachLayerAtCoordinate( + coordinate, frameState, callback, thisArg); + if (result) { + return result; + } + } + } + return undefined; +}; diff --git a/src/ol/renderer/canvas/tilelayer.js b/src/ol/renderer/canvas/tilelayer.js index bf502c2bc6..06b6aa6b98 100644 --- a/src/ol/renderer/canvas/tilelayer.js +++ b/src/ol/renderer/canvas/tilelayer.js @@ -249,7 +249,7 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function( /** - * @param {ol.Pixel} pixel Pixel. + * @param {ol.Coordinate} coordinate Coordinate. * @param {olx.FrameState} frameState FrameState. * @param {function(this: S, ol.layer.Layer, (Uint8ClampedArray|Uint8Array)): T} callback Layer * callback. @@ -257,9 +257,8 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function( * @return {T|undefined} Callback result. * @template S,T,U */ -ol.renderer.canvas.TileLayer.prototype.forEachLayerAtPixel = function( - pixel, frameState, callback, thisArg) { - var coordinate = ol.transform.apply(frameState.pixelToCoordinateTransform, pixel.slice()); +ol.renderer.canvas.TileLayer.prototype.forEachLayerAtCoordinate = function( + coordinate, frameState, callback, thisArg) { var canvasPixel = ol.transform.apply(this.coordinateToCanvasPixelTransform_, coordinate); var imageData = this.context.getImageData(canvasPixel[0], canvasPixel[1], 1, 1).data; diff --git a/src/ol/renderer/layer.js b/src/ol/renderer/layer.js index f6c603b8a4..2e96ea86d3 100644 --- a/src/ol/renderer/layer.js +++ b/src/ol/renderer/layer.js @@ -9,7 +9,6 @@ goog.require('ol.events'); goog.require('ol.events.EventType'); goog.require('ol.functions'); goog.require('ol.source.State'); -goog.require('ol.transform'); /** @@ -45,29 +44,6 @@ ol.inherits(ol.renderer.Layer, ol.Observable); ol.renderer.Layer.prototype.forEachFeatureAtCoordinate = ol.nullFunction; -/** - * @param {ol.Pixel} pixel Pixel. - * @param {olx.FrameState} frameState Frame state. - * @param {function(this: S, ol.layer.Layer, (Uint8ClampedArray|Uint8Array)): T} callback Layer callback. - * @param {S} thisArg Value to use as `this` when executing `callback`. - * @return {T|undefined} Callback result. - * @template S,T - */ -ol.renderer.Layer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) { - var coordinate = ol.transform.apply( - frameState.pixelToCoordinateTransform, pixel.slice()); - - var hasFeature = this.forEachFeatureAtCoordinate( - coordinate, frameState, ol.functions.TRUE, this); - - if (hasFeature) { - return callback.call(thisArg, this.layer_, null); - } else { - return undefined; - } -}; - - /** * @param {ol.Coordinate} coordinate Coordinate. * @param {olx.FrameState} frameState Frame state. diff --git a/src/ol/renderer/map.js b/src/ol/renderer/map.js index a8e88f804e..e9f0d8bae0 100644 --- a/src/ol/renderer/map.js +++ b/src/ol/renderer/map.js @@ -167,6 +167,7 @@ ol.renderer.Map.prototype.forEachFeatureAtCoordinate = function(coordinate, fram /** + * @abstract * @param {ol.Pixel} pixel Pixel. * @param {olx.FrameState} frameState FrameState. * @param {function(this: S, ol.layer.Layer, (Uint8ClampedArray|Uint8Array)): T} callback Layer @@ -181,29 +182,7 @@ ol.renderer.Map.prototype.forEachFeatureAtCoordinate = function(coordinate, fram * @template S,T,U */ ol.renderer.Map.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg, - layerFilter, thisArg2) { - var result; - var viewState = frameState.viewState; - var viewResolution = viewState.resolution; - - var layerStates = frameState.layerStatesArray; - var numLayers = layerStates.length; - var i; - for (i = numLayers - 1; i >= 0; --i) { - var layerState = layerStates[i]; - var layer = layerState.layer; - if (ol.layer.Layer.visibleAtResolution(layerState, viewResolution) && - layerFilter.call(thisArg2, layer)) { - var layerRenderer = this.getLayerRenderer(layer); - result = layerRenderer.forEachLayerAtPixel( - pixel, frameState, callback, thisArg); - if (result) { - return result; - } - } - } - return undefined; -}; + layerFilter, thisArg2) {}; /** diff --git a/src/ol/renderer/webgl/imagelayer.js b/src/ol/renderer/webgl/imagelayer.js index 8e7507ef9d..8df0c036df 100644 --- a/src/ol/renderer/webgl/imagelayer.js +++ b/src/ol/renderer/webgl/imagelayer.js @@ -219,13 +219,7 @@ ol.renderer.webgl.ImageLayer.prototype.hasFeatureAtCoordinate = function(coordin /** - * @param {ol.Pixel} pixel Pixel. - * @param {olx.FrameState} frameState FrameState. - * @param {function(this: S, ol.layer.Layer, (Uint8ClampedArray|Uint8Array)): T} callback Layer - * callback. - * @param {S} thisArg Value to use as `this` when executing `callback`. - * @return {T|undefined} Callback result. - * @template S,T,U + * @inheritDoc */ ol.renderer.webgl.ImageLayer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) { if (!this.image_ || !this.image_.getImage()) { diff --git a/src/ol/renderer/webgl/layer.js b/src/ol/renderer/webgl/layer.js index e6f7da508d..1dd5ae61dd 100644 --- a/src/ol/renderer/webgl/layer.js +++ b/src/ol/renderer/webgl/layer.js @@ -251,3 +251,16 @@ ol.renderer.webgl.Layer.prototype.handleWebGLContextLost = function() { * @return {boolean} whether composeFrame should be called. */ ol.renderer.webgl.Layer.prototype.prepareFrame = function(frameState, layerState, context) {}; + + +/** + * @abstract + * @param {ol.Pixel} pixel Pixel. + * @param {olx.FrameState} frameState FrameState. + * @param {function(this: S, ol.layer.Layer, (Uint8ClampedArray|Uint8Array)): T} callback Layer + * callback. + * @param {S} thisArg Value to use as `this` when executing `callback`. + * @return {T|undefined} Callback result. + * @template S,T,U + */ +ol.renderer.webgl.Layer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) {}; diff --git a/src/ol/renderer/webgl/map.js b/src/ol/renderer/webgl/map.js index e055609ff6..5844610adf 100644 --- a/src/ol/renderer/webgl/map.js +++ b/src/ol/renderer/webgl/map.js @@ -584,7 +584,7 @@ ol.renderer.webgl.Map.prototype.forEachLayerAtPixel = function(pixel, frameState var layer = layerState.layer; if (ol.layer.Layer.visibleAtResolution(layerState, viewState.resolution) && layerFilter.call(thisArg, layer)) { - var layerRenderer = this.getLayerRenderer(layer); + var layerRenderer = /** @type {ol.renderer.webgl.Layer} */ (this.getLayerRenderer(layer)); result = layerRenderer.forEachLayerAtPixel( pixel, frameState, callback, thisArg); if (result) { diff --git a/src/ol/renderer/webgl/tilelayer.js b/src/ol/renderer/webgl/tilelayer.js index b7e958a1b2..824f7d3303 100644 --- a/src/ol/renderer/webgl/tilelayer.js +++ b/src/ol/renderer/webgl/tilelayer.js @@ -358,13 +358,7 @@ ol.renderer.webgl.TileLayer.prototype.prepareFrame = function(frameState, layerS /** - * @param {ol.Pixel} pixel Pixel. - * @param {olx.FrameState} frameState FrameState. - * @param {function(this: S, ol.layer.Layer, (Uint8ClampedArray|Uint8Array)): T} callback Layer - * callback. - * @param {S} thisArg Value to use as `this` when executing `callback`. - * @return {T|undefined} Callback result. - * @template S,T,U + * @inheritDoc */ ol.renderer.webgl.TileLayer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) { if (!this.framebuffer) { diff --git a/src/ol/renderer/webgl/vectorlayer.js b/src/ol/renderer/webgl/vectorlayer.js index 494d5eb149..d94975f8a6 100644 --- a/src/ol/renderer/webgl/vectorlayer.js +++ b/src/ol/renderer/webgl/vectorlayer.js @@ -154,13 +154,7 @@ ol.renderer.webgl.VectorLayer.prototype.hasFeatureAtCoordinate = function(coordi /** - * @param {ol.Pixel} pixel Pixel. - * @param {olx.FrameState} frameState FrameState. - * @param {function(this: S, ol.layer.Layer, (Uint8ClampedArray|Uint8Array)): T} callback Layer - * callback. - * @param {S} thisArg Value to use as `this` when executing `callback`. - * @return {T|undefined} Callback result. - * @template S,T,U + * @inheritDoc */ ol.renderer.webgl.VectorLayer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) { var coordinate = ol.transform.apply( From 0d65d1d8137dbb42882c7c5e3ec5c6ad81b9b10a Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Mon, 7 Nov 2016 00:29:53 +0100 Subject: [PATCH 3/5] Remove support for skipped features --- src/ol/typedefs.js | 3 +-- src/ol/vectortile.js | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ol/typedefs.js b/src/ol/typedefs.js index 68782fbf3c..10b6cc70e8 100644 --- a/src/ol/typedefs.js +++ b/src/ol/typedefs.js @@ -633,8 +633,7 @@ ol.TilePriorityFunction; * renderedRenderOrder: (null|function(ol.Feature, ol.Feature):number), * renderedTileRevision: number, * renderedRevision: number, - * replayGroup: ol.render.ReplayGroup, - * skippedFeatures: Array.}} + * replayGroup: ol.render.ReplayGroup}} */ ol.TileReplayState; diff --git a/src/ol/vectortile.js b/src/ol/vectortile.js index 04efabc99a..66ce5df006 100644 --- a/src/ol/vectortile.js +++ b/src/ol/vectortile.js @@ -59,8 +59,7 @@ ol.VectorTile = function(tileCoord, state, src, format, tileLoadFunction) { renderedRenderOrder: null, renderedRevision: -1, renderedTileRevision: -1, - replayGroup: null, - skippedFeatures: [] + replayGroup: null }; /** From ee7b89435041bf518a7d0b29fb1464a6f2f8b71c Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Mon, 7 Nov 2016 09:37:09 +0100 Subject: [PATCH 4/5] Refactor VectorTile renderer and add lower resolution clipping --- src/ol/render/canvas/replaygroup.js | 26 +- src/ol/renderer/canvas/layer.js | 10 +- src/ol/renderer/canvas/tilelayer.js | 134 ++++---- src/ol/renderer/canvas/vectorlayer.js | 4 +- src/ol/renderer/canvas/vectortilelayer.js | 293 ++++++++---------- .../spec/ol/renderer/canvas/tilelayer.test.js | 12 +- .../renderer/canvas/vectortilelayer.test.js | 8 +- .../2-layers-canvas-extent-rotate-hidpi.png | Bin 11881 -> 12689 bytes 8 files changed, 228 insertions(+), 259 deletions(-) diff --git a/src/ol/render/canvas/replaygroup.js b/src/ol/render/canvas/replaygroup.js index 40a7722be2..fae6101bd2 100644 --- a/src/ol/render/canvas/replaygroup.js +++ b/src/ol/render/canvas/replaygroup.js @@ -149,6 +149,23 @@ ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function( }; +/** + * @param {ol.Transform} transform Transform. + * @return {Array.} Clip coordinates. + */ +ol.render.canvas.ReplayGroup.prototype.getClipCoords = function(transform) { + 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); + return flatClipCoords; +}; + + /** * @inheritDoc */ @@ -200,14 +217,7 @@ ol.render.canvas.ReplayGroup.prototype.replay = function(context, pixelRatio, // 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); + var flatClipCoords = this.getClipCoords(transform); context.save(); context.beginPath(); context.moveTo(flatClipCoords[0], flatClipCoords[1]); diff --git a/src/ol/renderer/canvas/layer.js b/src/ol/renderer/canvas/layer.js index 5ffb9e1f93..39699128a3 100644 --- a/src/ol/renderer/canvas/layer.js +++ b/src/ol/renderer/canvas/layer.js @@ -69,7 +69,7 @@ ol.renderer.canvas.Layer.prototype.clip = function(context, frameState, extent) */ ol.renderer.canvas.Layer.prototype.composeFrame = function(frameState, layerState, context) { - this.dispatchPreComposeEvent(context, frameState); + this.preCompose(context, frameState); var image = this.getImage(); if (image) { @@ -103,8 +103,7 @@ ol.renderer.canvas.Layer.prototype.composeFrame = function(frameState, layerStat } } - this.dispatchPostComposeEvent(context, frameState); - + this.postCompose(context, frameState, layerState); }; @@ -138,10 +137,11 @@ ol.renderer.canvas.Layer.prototype.dispatchComposeEvent_ = function(type, contex /** * @param {CanvasRenderingContext2D} context Context. * @param {olx.FrameState} frameState Frame state. + * @param {ol.LayerState} layerState Layer state. * @param {ol.Transform=} opt_transform Transform. * @protected */ -ol.renderer.canvas.Layer.prototype.dispatchPostComposeEvent = function(context, frameState, opt_transform) { +ol.renderer.canvas.Layer.prototype.postCompose = function(context, frameState, layerState, opt_transform) { this.dispatchComposeEvent_(ol.render.Event.Type.POSTCOMPOSE, context, frameState, opt_transform); }; @@ -153,7 +153,7 @@ ol.renderer.canvas.Layer.prototype.dispatchPostComposeEvent = function(context, * @param {ol.Transform=} opt_transform Transform. * @protected */ -ol.renderer.canvas.Layer.prototype.dispatchPreComposeEvent = function(context, frameState, opt_transform) { +ol.renderer.canvas.Layer.prototype.preCompose = function(context, frameState, opt_transform) { this.dispatchComposeEvent_(ol.render.Event.Type.PRECOMPOSE, context, frameState, opt_transform); }; diff --git a/src/ol/renderer/canvas/tilelayer.js b/src/ol/renderer/canvas/tilelayer.js index 06b6aa6b98..5ff073e9ad 100644 --- a/src/ol/renderer/canvas/tilelayer.js +++ b/src/ol/renderer/canvas/tilelayer.js @@ -27,18 +27,6 @@ ol.renderer.canvas.TileLayer = function(tileLayer) { */ this.context = ol.dom.createCanvasContext2D(); - /** - * @private - * @type {ol.Extent} - */ - this.renderedExtent_ = ol.extent.createEmpty(); - - /** - * @private - * @type {number} - */ - this.renderedResolution_; - /** * @private * @type {number} @@ -47,9 +35,15 @@ ol.renderer.canvas.TileLayer = function(tileLayer) { /** * @private - * @type {Array.} + * @type {ol.TileRange} */ - this.renderedTileKeys_ = null; + this.renderedTileRange_ = null; + + /** + * @protected + * @type {!Array.} + */ + this.renderedTiles = []; /** * @protected @@ -63,6 +57,12 @@ ol.renderer.canvas.TileLayer = function(tileLayer) { */ this.tmpTileCoord_ = [0, 0, 0]; + /** + * @private + * @type {ol.TileRange} + */ + this.tmpTileRange_ = new ol.TileRange(0, 0, 0, 0); + /** * @private * @type {ol.Transform} @@ -88,8 +88,7 @@ ol.inherits(ol.renderer.canvas.TileLayer, ol.renderer.canvas.Layer); /** * @inheritDoc */ -ol.renderer.canvas.TileLayer.prototype.prepareFrame = function( - frameState, layerState) { +ol.renderer.canvas.TileLayer.prototype.prepareFrame = function(frameState, layerState) { var pixelRatio = frameState.pixelRatio; var size = frameState.size; @@ -121,13 +120,13 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function( var tilePixelRatio = tileSource.getTilePixelRatio(pixelRatio); var scale = pixelRatio / tilePixelRatio * tileResolution / viewResolution; - var transform = ol.transform.compose(ol.transform.reset(this.imageTransform_), + var transform = ol.transform.compose(this.imageTransform_, pixelRatio * size[0] / 2, pixelRatio * size[1] / 2, scale, scale, 0, tilePixelRatio * (imageExtent[0] - viewCenter[0]) / tileResolution, tilePixelRatio * (viewCenter[1] - imageExtent[3]) / tileResolution); - ol.transform.compose(ol.transform.reset(this.coordinateToCanvasPixelTransform_), + ol.transform.compose(this.coordinateToCanvasPixelTransform_, pixelRatio * size[0] / 2 - transform[4], pixelRatio * size[1] / 2 - transform[5], pixelRatio / viewResolution, -pixelRatio / viewResolution, 0, @@ -138,45 +137,38 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function( */ var tilesToDrawByZ = {}; tilesToDrawByZ[z] = {}; - var loadedTileKeys = []; var findLoadedTiles = this.createLoadedTileFinder( tileSource, projection, tilesToDrawByZ); var useInterimTilesOnError = tileLayer.getUseInterimTilesOnError(); - var tmpExtent = this.tmpExtent; - var tmpTileRange = new ol.TileRange(0, 0, 0, 0); - var childTileRange, fullyLoaded, tile, tileCoordKey, x, y; - var drawableTile = ( - /** - * @param {!ol.Tile} tile Tile. - * @return {boolean} Tile is selected. - */ - function(tile) { - var tileState = tile.getState(); - return tileState == ol.Tile.State.LOADED || - tileState == ol.Tile.State.EMPTY || - tileState == ol.Tile.State.ERROR && !useInterimTilesOnError; - }); + var tmpTileRange = this.tmpTileRange_; + var newTiles = false; + var tile, x, y; for (x = tileRange.minX; x <= tileRange.maxX; ++x) { for (y = tileRange.minY; y <= tileRange.maxY; ++y) { tile = tileSource.getTile(z, x, y, pixelRatio, projection); - if (!drawableTile(tile)) { + var tileState = tile.getState(); + var drawable = tileState == ol.Tile.State.LOADED || + tileState == ol.Tile.State.EMPTY || + tileState == ol.Tile.State.ERROR && !useInterimTilesOnError; + if (!drawable) { tile = tile.getInterimTile(); - } - if (drawableTile(tile)) { - if (tile.getState() == ol.Tile.State.LOADED) { - loadedTileKeys.push(tileCoordKey); - tileCoordKey = tile.tileCoord.toString(); - tilesToDrawByZ[z][tileCoordKey] = tile; + } else { + if (tileState == ol.Tile.State.LOADED) { + tilesToDrawByZ[z][tile.tileCoord.toString()] = tile; + if (!newTiles && this.renderedTiles.indexOf(tile) == -1) { + newTiles = true; + } } continue; } - fullyLoaded = tileGrid.forEachTileCoordParentTileRange( + + var fullyLoaded = tileGrid.forEachTileCoordParentTileRange( tile.tileCoord, findLoadedTiles, null, tmpTileRange, tmpExtent); if (!fullyLoaded) { - childTileRange = tileGrid.getTileCoordChildTileRange( + var childTileRange = tileGrid.getTileCoordChildTileRange( tile.tileCoord, tmpTileRange, tmpExtent); if (childTileRange) { findLoadedTiles(z + 1, childTileRange); @@ -185,12 +177,10 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function( } } - loadedTileKeys.sort(); - if (tileResolution != this.renderedResolution_ || - sourceRevision != this.renderedRevision_ || - !ol.extent.equals(this.renderedExtent_, imageExtent) || - !ol.array.equals(loadedTileKeys, this.renderedTileKeys_)) { + if (newTiles || !(this.renderedTileRange_ && + this.renderedTileRange_.equals(tileRange)) || + this.renderedRevision_ != sourceRevision) { var tilePixelSize = tileSource.getTilePixelSize(z, pixelRatio, projection); var width = tileRange.getWidth() * tilePixelSize[0]; @@ -203,11 +193,12 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function( canvas.height = height; } + this.renderedTiles.length = 0; /** @type {Array.} */ var zs = Object.keys(tilesToDrawByZ).map(Number); zs.sort(ol.array.numberSafeCompareFunction); var currentResolution, currentScale, currentTilePixelSize, currentZ, i, ii; - var image, tileExtent, tileGutter, tilesToDraw, w, h; + var tileExtent, tileGutter, tilesToDraw, w, h; for (i = 0, ii = zs.length; i < ii; ++i) { currentZ = zs[i]; currentTilePixelSize = tileSource.getTilePixelSize(currentZ, pixelRatio, projection); @@ -215,7 +206,7 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function( currentScale = currentResolution / tileResolution; tileGutter = tilePixelRatio * tileSource.getGutter(projection); tilesToDraw = tilesToDrawByZ[currentZ]; - for (tileCoordKey in tilesToDraw) { + for (var tileCoordKey in tilesToDraw) { tile = tilesToDraw[tileCoordKey]; tileExtent = tileGrid.getTileCoordExtent(tile.getTileCoord(), tmpExtent); x = (tileExtent[0] - imageExtent[0]) / tileResolution * tilePixelRatio; @@ -225,12 +216,13 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function( if (!opaque) { context.clearRect(x, y, w, h); } - image = tile.getImage(); - context.drawImage(image, tileGutter, tileGutter, - image.width, image.height, x, y, w, h); + this.drawTileImage(tile, frameState, layerState, x, y, w, h, tileGutter); + this.renderedTiles.push(tile); } } + this.renderedTileRange_ = tileRange; + this.renderedRevision_ = sourceRevision; } this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange); @@ -239,12 +231,26 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function( this.scheduleExpireCache(frameState, tileSource); this.updateLogos(frameState, tileSource); - this.renderedTileKeys_ = loadedTileKeys; - this.renderedExtent_ = imageExtent; - this.renderedResolution_ = tileResolution; - this.renderedRevision_ = sourceRevision; + return this.renderedTiles.length > 0; +}; - return true; + +/** + * @param {ol.Tile} tile Tile. + * @param {olx.FrameState} frameState Frame state. + * @param {ol.LayerState} layerState Layer state. + * @param {number} x Left of the tile. + * @param {number} y Top of the tile. + * @param {number} w Width of the tile. + * @param {number} h Height of the tile. + * @param {number} gutter Tile gutter. + */ +ol.renderer.canvas.TileLayer.prototype.drawTileImage = function(tile, frameState, layerState, x, y, w, h, gutter) { + var image = tile.getImage(); + if (image) { + this.context.drawImage(image, gutter, gutter, + image.width - 2 * gutter, image.height - 2 * gutter, x, y, w, h); + } }; @@ -279,16 +285,16 @@ ol.renderer.canvas.TileLayer.prototype.getImage = function() { }; +/** + * @function + * @return {ol.layer.Tile|ol.layer.VectorTile} + */ +ol.renderer.canvas.TileLayer.prototype.getLayer; + + /** * @inheritDoc */ ol.renderer.canvas.TileLayer.prototype.getImageTransform = function() { return this.imageTransform_; }; - - -/** - * @function - * @return {ol.layer.Tile|ol.layer.VectorTile} - */ -ol.renderer.canvas.TileLayer.prototype.getLayer; diff --git a/src/ol/renderer/canvas/vectorlayer.js b/src/ol/renderer/canvas/vectorlayer.js index 8addea4e39..cd375e0f94 100644 --- a/src/ol/renderer/canvas/vectorlayer.js +++ b/src/ol/renderer/canvas/vectorlayer.js @@ -83,7 +83,7 @@ ol.renderer.canvas.VectorLayer.prototype.composeFrame = function(frameState, lay var transform = this.getTransform(frameState, 0); - this.dispatchPreComposeEvent(context, frameState, transform); + this.preCompose(context, frameState, transform); // clipped rendering if layer extent is set var clipExtent = layerState.extent; @@ -169,7 +169,7 @@ ol.renderer.canvas.VectorLayer.prototype.composeFrame = function(frameState, lay if (clipped) { context.restore(); } - this.dispatchPostComposeEvent(context, frameState, transform); + this.postCompose(context, frameState, layerState, transform); }; diff --git a/src/ol/renderer/canvas/vectortilelayer.js b/src/ol/renderer/canvas/vectortilelayer.js index 1e08c8e00c..ee4e14a14c 100644 --- a/src/ol/renderer/canvas/vectortilelayer.js +++ b/src/ol/renderer/canvas/vectortilelayer.js @@ -1,12 +1,10 @@ goog.provide('ol.renderer.canvas.VectorTileLayer'); goog.require('ol'); -goog.require('ol.array'); goog.require('ol.extent'); goog.require('ol.proj'); goog.require('ol.proj.Units'); goog.require('ol.layer.VectorTile'); -goog.require('ol.render.Event'); goog.require('ol.render.ReplayType'); goog.require('ol.render.canvas'); goog.require('ol.render.canvas.ReplayGroup'); @@ -66,126 +64,12 @@ ol.renderer.canvas.VectorTileLayer.VECTOR_REPLAYS = { }; -/** - * @inheritDoc - */ -ol.renderer.canvas.VectorTileLayer.prototype.composeFrame = function( - frameState, layerState, context) { - var transform = this.getTransform(frameState, 0); - this.dispatchPreComposeEvent(context, frameState, transform); - - // clipped rendering if layer extent is set - var extent = layerState.extent; - var clipped = extent !== undefined; - if (clipped) { - this.clip(context, frameState, /** @type {ol.Extent} */ (extent)); - } - - var renderMode = this.getLayer().getRenderMode(); - if (renderMode !== ol.layer.VectorTile.RenderType.VECTOR) { - this.renderTileImages(context, frameState, layerState); - } - if (renderMode !== ol.layer.VectorTile.RenderType.IMAGE) { - this.renderTileReplays_(context, frameState, layerState); - } - - if (clipped) { - context.restore(); - } - - this.dispatchPostComposeEvent(context, frameState, transform); -}; - - -/** - * @param {CanvasRenderingContext2D} context Context. - * @param {olx.FrameState} frameState Frame state. - * @param {ol.LayerState} layerState Layer state. - * @private - */ -ol.renderer.canvas.VectorTileLayer.prototype.renderTileReplays_ = function( - context, frameState, layerState) { - - var layer = this.getLayer(); - var replays = ol.renderer.canvas.VectorTileLayer.VECTOR_REPLAYS[layer.getRenderMode()]; - var pixelRatio = frameState.pixelRatio; - var skippedFeatureUids = layerState.managed ? - frameState.skippedFeatureUids : {}; - var viewState = frameState.viewState; - var center = viewState.center; - var resolution = viewState.resolution; - var rotation = viewState.rotation; - var size = frameState.size; - var pixelScale = pixelRatio / resolution; - var source = /** @type {ol.source.VectorTile} */ (layer.getSource()); - var tilePixelRatio = source.getTilePixelRatio(); - - var transform = this.getTransform(frameState, 0); - - var replayContext; - if (layer.hasListener(ol.render.Event.Type.RENDER)) { - // resize and clear - this.context.canvas.width = context.canvas.width; - this.context.canvas.height = context.canvas.height; - replayContext = this.context; - } else { - replayContext = context; - } - // for performance reasons, context.save / context.restore is not used - // to save and restore the transformation matrix and the opacity. - // see http://jsperf.com/context-save-restore-versus-variable - var alpha = replayContext.globalAlpha; - replayContext.globalAlpha = layerState.opacity; - - /** @type {Array.} */ - var tilesToDraw = this.renderedTiles; - - var tileGrid = source.getTileGrid(); - - var currentZ, i, ii, offsetX, offsetY, origin, pixelSpace, replayState; - var tile, tileExtent, tilePixelResolution, tileResolution, tileTransform; - 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]; - pixelSpace = tile.getProjection().getUnits() == ol.proj.Units.TILE_PIXELS; - tileResolution = tileGrid.getResolution(currentZ); - tilePixelResolution = tileResolution / tilePixelRatio; - offsetX = Math.round(pixelRatio * size[0] / 2); - offsetY = Math.round(pixelRatio * size[1] / 2); - - if (pixelSpace) { - origin = ol.extent.getTopLeft(tileExtent); - tileTransform = ol.transform.reset(this.tmpTransform_); - tileTransform = ol.transform.compose(this.tmpTransform_, - offsetX, offsetY, - pixelScale * tilePixelResolution, pixelScale * tilePixelResolution, - rotation, - (origin[0] - center[0]) / tilePixelResolution, (center[1] - origin[1]) / tilePixelResolution); - } else { - tileTransform = transform; - } - ol.render.canvas.rotateAtOffset(replayContext, -rotation, offsetX, offsetY); - replayState.replayGroup.replay(replayContext, pixelRatio, - tileTransform, rotation, skippedFeatureUids, replays); - ol.render.canvas.rotateAtOffset(replayContext, rotation, offsetX, offsetY); - } - - if (replayContext != context) { - this.dispatchRenderEvent(replayContext, frameState, transform); - context.drawImage(replayContext.canvas, 0, 0); - } - replayContext.globalAlpha = alpha; -}; - - /** * @param {ol.VectorTile} tile Tile. * @param {olx.FrameState} frameState Frame state. + * @private */ -ol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup = function(tile, +ol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup_ = function(tile, frameState) { var layer = this.getLayer(); var pixelRatio = frameState.pixelRatio; @@ -204,12 +88,11 @@ ol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup = function(tile, var source = /** @type {ol.source.VectorTile} */ (layer.getSource()); var tileGrid = source.getTileGrid(); - var tileCoord = tile.getTileCoord(); + var tileCoord = tile.tileCoord; var tileProjection = tile.getProjection(); - var pixelSpace = tileProjection.getUnits() == ol.proj.Units.TILE_PIXELS; var resolution = tileGrid.getResolution(tileCoord[0]); var extent, reproject, tileResolution; - if (pixelSpace) { + if (tileProjection.getUnits() == ol.proj.Units.TILE_PIXELS) { var tilePixelRatio = tileResolution = source.getTilePixelRatio(); var tileSize = ol.size.toSize(tileGrid.getTileSize(tileCoord[0])); extent = [0, 0, tileSize[0] * tilePixelRatio, tileSize[1] * tilePixelRatio]; @@ -275,6 +158,21 @@ ol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup = function(tile, }; +/** + * @inheritDoc + */ +ol.renderer.canvas.VectorTileLayer.prototype.drawTileImage = function( + tile, frameState, layerState, x, y, w, h, gutter) { + var vectorTile = /** @type {ol.VectorTile} */ (tile); + this.createReplayGroup_(vectorTile, frameState); + var layer = this.getLayer(); + if (layer.getRenderMode() != ol.layer.VectorTile.RenderType.VECTOR) { + this.renderTileImage_(vectorTile, frameState, layerState); + } + ol.renderer.canvas.TileLayer.prototype.drawTileImage.apply(this, arguments); +}; + + /** * @inheritDoc */ @@ -295,9 +193,8 @@ ol.renderer.canvas.VectorTileLayer.prototype.forEachFeatureAtCoordinate = functi var tile, tileCoord, tileExtent, tilePixelRatio, tileResolution; for (i = 0, ii = replayables.length; i < ii; ++i) { tile = replayables[i]; - tileCoord = tile.getTileCoord(); - tileExtent = source.getTileGrid().getTileCoordExtent(tileCoord, - this.tmpExtent); + tileCoord = tile.tileCoord; + tileExtent = source.getTileGrid().getTileCoordExtent(tileCoord, this.tmpExtent); if (!ol.extent.containsCoordinate(tileExtent, coordinate)) { continue; } @@ -332,6 +229,41 @@ ol.renderer.canvas.VectorTileLayer.prototype.forEachFeatureAtCoordinate = functi }; +/** + * @param {ol.Tile} tile Tile. + * @param {olx.FrameState} frameState Frame state. + * @return {ol.Transform} transform Transform. + * @private + */ +ol.renderer.canvas.VectorTileLayer.prototype.getReplayTransform_ = function(tile, frameState) { + if (tile.getProjection().getUnits() == ol.proj.Units.TILE_PIXELS) { + var layer = this.getLayer(); + var source = /** @type {ol.source.VectorTile} */ (layer.getSource()); + var tileGrid = source.getTileGrid(); + var tileCoord = tile.tileCoord; + var tileResolution = + tileGrid.getResolution(tileCoord[0]) / source.getTilePixelRatio(); + var viewState = frameState.viewState; + var pixelRatio = frameState.pixelRatio; + var renderResolution = viewState.resolution / pixelRatio; + var tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent); + var center = viewState.center; + var origin = ol.extent.getTopLeft(tileExtent); + var size = frameState.size; + var offsetX = Math.round(pixelRatio * size[0] / 2); + var offsetY = Math.round(pixelRatio * size[1] / 2); + return ol.transform.compose(this.tmpTransform_, + offsetX, offsetY, + tileResolution / renderResolution, tileResolution / renderResolution, + viewState.rotation, + (origin[0] - center[0]) / tileResolution, + (center[1] - origin[1]) / tileResolution); + } else { + return this.getTransform(frameState, 0); + } +}; + + /** * Handle changes in image style state. * @param {ol.events.Event} event Image style change event. @@ -345,17 +277,53 @@ ol.renderer.canvas.VectorTileLayer.prototype.handleStyleImageChange_ = function( /** * @inheritDoc */ -ol.renderer.canvas.VectorTileLayer.prototype.prepareFrame = function(frameState, layerState) { - var prepared = ol.renderer.canvas.TileLayer.prototype.prepareFrame.call(this, frameState, layerState); - if (prepared) { - var skippedFeatures = Object.keys(frameState.skippedFeatureUids_ || {}); - for (var i = 0, ii = this.renderedTiles.length; i < ii; ++i) { - var tile = /** @type {ol.VectorTile} */ (this.renderedTiles[i]); - this.createReplayGroup(tile, frameState); - this.renderTileImage_(tile, frameState, layerState, skippedFeatures); +ol.renderer.canvas.VectorTileLayer.prototype.postCompose = function(context, frameState, layerState) { + var renderMode = this.getLayer().getRenderMode(); + var replays = ol.renderer.canvas.VectorTileLayer.VECTOR_REPLAYS[renderMode]; + if (replays) { + var pixelRatio = frameState.pixelRatio; + var rotation = frameState.viewState.rotation; + var size = frameState.size; + var offsetX = Math.round(pixelRatio * size[0] / 2); + var offsetY = Math.round(pixelRatio * size[1] / 2); + var tiles = this.renderedTiles; + var clips = []; + var zs = []; + for (var i = tiles.length - 1; i >= 0; --i) { + var tile = /** @type {ol.VectorTile} */ (tiles[i]); + // Create a clip mask for regions in this low resolution tile that are + // already filled by a higher resolution tile + var transform = this.getReplayTransform_(tile, frameState); + var currentClip = tile.getReplayState().replayGroup.getClipCoords(transform); + var currentZ = tile.tileCoord[0]; + context.save(); + context.globalAlpha = layerState.opacity; + ol.render.canvas.rotateAtOffset(context, -rotation, offsetX, offsetY); + for (var j = 0, jj = clips.length; j < jj; ++j) { + var clip = clips[j]; + if (currentZ < zs[j]) { + context.beginPath(); + // counter-clockwise (outer ring) for current tile + context.moveTo(currentClip[0], currentClip[1]); + context.lineTo(currentClip[2], currentClip[3]); + context.lineTo(currentClip[4], currentClip[5]); + context.lineTo(currentClip[6], currentClip[7]); + // clockwise (inner ring) for higher resolution tile + context.moveTo(clip[6], clip[7]); + context.lineTo(clip[4], clip[5]); + context.lineTo(clip[2], clip[3]); + context.lineTo(clip[0], clip[1]); + context.clip(); + } + } + var replayGroup = tile.getReplayState().replayGroup; + replayGroup.replay(context, pixelRatio, transform, rotation, {}, replays); + context.restore(); + clips.push(currentClip); + zs.push(currentZ); } } - return prepared; + ol.renderer.canvas.TileLayer.prototype.postCompose.apply(this, arguments); }; @@ -391,57 +359,38 @@ ol.renderer.canvas.VectorTileLayer.prototype.renderFeature = function(feature, s * @param {ol.VectorTile} tile Tile. * @param {olx.FrameState} frameState Frame state. * @param {ol.LayerState} layerState Layer state. - * @param {Array.} skippedFeatures Skipped features. * @private */ ol.renderer.canvas.VectorTileLayer.prototype.renderTileImage_ = function( - tile, frameState, layerState, skippedFeatures) { + tile, frameState, layerState) { var layer = this.getLayer(); - var replays = ol.renderer.canvas.VectorTileLayer.IMAGE_REPLAYS[layer.getRenderMode()]; - if (!replays) { - // do not create an image in 'vector' mode - return; - } - var pixelRatio = frameState.pixelRatio; var replayState = tile.getReplayState(); var revision = layer.getRevision(); - if (!ol.array.equals(replayState.skippedFeatures, skippedFeatures) || - replayState.renderedTileRevision !== revision) { - replayState.skippedFeatures = skippedFeatures; + var replays = ol.renderer.canvas.VectorTileLayer.IMAGE_REPLAYS[layer.getRenderMode()]; + if (replays && replayState.renderedTileRevision !== revision) { replayState.renderedTileRevision = revision; - var tileContext = tile.getContext(); + var tileCoord = tile.tileCoord; + var z = tile.tileCoord[0]; + var pixelRatio = frameState.pixelRatio; var source = layer.getSource(); var tileGrid = source.getTileGrid(); - var currentZ = tile.getTileCoord()[0]; - var resolution = tileGrid.getResolution(currentZ); - var tileSize = ol.size.toSize(tileGrid.getTileSize(currentZ)); - var tileResolution = tileGrid.getResolution(currentZ); - var resolutionRatio = tileResolution / resolution; - var width = tileSize[0] * pixelRatio * resolutionRatio; - var height = tileSize[1] * pixelRatio * resolutionRatio; - tileContext.canvas.width = width / resolutionRatio + 0.5; - tileContext.canvas.height = height / resolutionRatio + 0.5; - tileContext.scale(1 / resolutionRatio, 1 / resolutionRatio); - tileContext.translate(width / 2, height / 2); - var pixelSpace = tile.getProjection().getUnits() == ol.proj.Units.TILE_PIXELS; - var pixelScale = pixelRatio / resolution; var tilePixelRatio = source.getTilePixelRatio(); - var tilePixelResolution = tileResolution / tilePixelRatio; - var tileExtent = tileGrid.getTileCoordExtent( - tile.getTileCoord(), this.tmpExtent); - var tileTransform = ol.transform.reset(this.tmpTransform_); - if (pixelSpace) { - ol.transform.scale(tileTransform, - pixelScale * tilePixelResolution, pixelScale * tilePixelResolution); - ol.transform.translate(tileTransform, - -tileSize[0] * tilePixelRatio / 2, -tileSize[1] * tilePixelRatio / 2); + var transform = ol.transform.reset(this.tmpTransform_); + if (tile.getProjection().getUnits() == ol.proj.Units.TILE_PIXELS) { + var renderPixelRatio = pixelRatio / tilePixelRatio; + ol.transform.scale(transform, renderPixelRatio, renderPixelRatio); } else { - var tileCenter = ol.extent.getCenter(tileExtent); - ol.transform.scale(tileTransform, pixelScale, -pixelScale); - ol.transform.translate(tileTransform, -tileCenter[0], -tileCenter[1]); + var resolution = tileGrid.getResolution(z); + var pixelScale = pixelRatio / resolution; + var tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent); + ol.transform.scale(transform, pixelScale, -pixelScale); + ol.transform.translate(transform, -tileExtent[0], -tileExtent[3]); } - replayState.replayGroup.replay(tileContext, pixelRatio, - tileTransform, 0, frameState.skippedFeatureUids || {}, replays); + var context = tile.getContext(); + var size = source.getTilePixelSize(z, pixelRatio, frameState.viewState.projection); + context.canvas.width = size[0]; + context.canvas.height = size[1]; + replayState.replayGroup.replay(context, pixelRatio, transform, 0, {}, replays); } }; diff --git a/test/spec/ol/renderer/canvas/tilelayer.test.js b/test/spec/ol/renderer/canvas/tilelayer.test.js index c7d483f19f..1ed14bee54 100644 --- a/test/spec/ol/renderer/canvas/tilelayer.test.js +++ b/test/spec/ol/renderer/canvas/tilelayer.test.js @@ -32,15 +32,18 @@ describe('ol.renderer.canvas.TileLayer', function() { renderer.renderedTiles = []; var frameState = { viewState: { - center: [2, 3], + center: [10, 5], projection: ol.proj.get('EPSG:3857'), resolution: 1, rotation: Math.PI }, - size: [10, 10], + extent: [0, 0, 20, 10], + size: [20, 10], pixelRatio: 2, coordinateToPixelTransform: ol.transform.create(), - pixelToCoordinateTransform: ol.transform.create() + pixelToCoordinateTransform: ol.transform.create(), + usedTiles: {}, + wantedTiles: {} }; renderer.getImageTransform = function() { return ol.transform.create(); @@ -62,8 +65,9 @@ describe('ol.renderer.canvas.TileLayer', function() { return img; } }]; + renderer.prepareFrame(frameState, layerState); renderer.composeFrame(frameState, layerState, context); - expect(context.drawImage.firstCall.args[0].width).to.be(112); + expect(context.drawImage.firstCall.args[0].width).to.be(17); }); }); diff --git a/test/spec/ol/renderer/canvas/vectortilelayer.test.js b/test/spec/ol/renderer/canvas/vectortilelayer.test.js index 4f54f8c8e3..7979fa3d19 100644 --- a/test/spec/ol/renderer/canvas/vectortilelayer.test.js +++ b/test/spec/ol/renderer/canvas/vectortilelayer.test.js @@ -89,7 +89,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { it('does not render images for pure vector rendering', function() { layer.renderMode_ = 'vector'; var spy = sinon.spy(ol.renderer.canvas.VectorTileLayer.prototype, - 'renderTileImages'); + 'renderTileImage_'); map.renderSync(); expect(spy.callCount).to.be(0); spy.restore(); @@ -98,7 +98,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { it('does not render replays for pure image rendering', function() { layer.renderMode_ = 'image'; var spy = sinon.spy(ol.renderer.canvas.VectorTileLayer.prototype, - 'renderTileReplays_'); + 'getReplayTransform_'); map.renderSync(); expect(spy.callCount).to.be(0); spy.restore(); @@ -106,9 +106,9 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { it('renders both replays and images for hybrid rendering', function() { var spy1 = sinon.spy(ol.renderer.canvas.VectorTileLayer.prototype, - 'renderTileReplays_'); + 'getReplayTransform_'); var spy2 = sinon.spy(ol.renderer.canvas.VectorTileLayer.prototype, - 'renderTileImages'); + 'renderTileImage_'); map.renderSync(); expect(spy1.callCount).to.be(1); expect(spy2.callCount).to.be(1); diff --git a/test_rendering/spec/ol/layer/expected/2-layers-canvas-extent-rotate-hidpi.png b/test_rendering/spec/ol/layer/expected/2-layers-canvas-extent-rotate-hidpi.png index 164e80b392818f65fee201b16206202b5435010c..d0b115cca4c03fb1b4cdc9782f39f1ce3a03b586 100644 GIT binary patch literal 12689 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 I;wB;g2Ysy?-T(jq literal 11881 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 Date: Tue, 6 Dec 2016 08:43:10 +0100 Subject: [PATCH 5/5] Defer frame preparation when too much time was spent --- src/ol/renderer/canvas/tilelayer.js | 55 ++++++++++++++++++----------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/src/ol/renderer/canvas/tilelayer.js b/src/ol/renderer/canvas/tilelayer.js index 5ff073e9ad..5c4ee410b9 100644 --- a/src/ol/renderer/canvas/tilelayer.js +++ b/src/ol/renderer/canvas/tilelayer.js @@ -29,15 +29,21 @@ ol.renderer.canvas.TileLayer = function(tileLayer) { /** * @private - * @type {number} + * @type {ol.Extent} */ - this.renderedRevision_; + this.renderedExtent_ = null; /** * @private - * @type {ol.TileRange} + * @type {number} */ - this.renderedTileRange_ = null; + this.renderedResolution_; + + /** + * @private + * @type {number} + */ + this.renderedRevision_; /** * @protected @@ -118,19 +124,6 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function(frameState, layer var imageExtent = tileGrid.getTileRangeExtent(z, tileRange); var tilePixelRatio = tileSource.getTilePixelRatio(pixelRatio); - var scale = pixelRatio / tilePixelRatio * tileResolution / viewResolution; - - var transform = ol.transform.compose(this.imageTransform_, - pixelRatio * size[0] / 2, pixelRatio * size[1] / 2, - scale, scale, - 0, - tilePixelRatio * (imageExtent[0] - viewCenter[0]) / tileResolution, - tilePixelRatio * (viewCenter[1] - imageExtent[3]) / tileResolution); - ol.transform.compose(this.coordinateToCanvasPixelTransform_, - pixelRatio * size[0] / 2 - transform[4], pixelRatio * size[1] / 2 - transform[5], - pixelRatio / viewResolution, -pixelRatio / viewResolution, - 0, - -viewCenter[0], -viewCenter[1]); /** * @type {Object.>} @@ -178,9 +171,12 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function(frameState, layer } } - if (newTiles || !(this.renderedTileRange_ && - this.renderedTileRange_.equals(tileRange)) || - this.renderedRevision_ != sourceRevision) { + var hints = frameState.viewHints; + if (!(this.renderedResolution_ && Date.now() - frameState.time > 16 && + (hints[ol.View.Hint.ANIMATING] || hints[ol.View.Hint.INTERACTING])) && + (newTiles || !(this.renderedExtent_ && + ol.extent.equals(this.renderedExtent_, imageExtent)) || + this.renderedRevision_ != sourceRevision)) { var tilePixelSize = tileSource.getTilePixelSize(z, pixelRatio, projection); var width = tileRange.getWidth() * tilePixelSize[0]; @@ -191,6 +187,8 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function(frameState, layer if (canvas.width != width || canvas.height != height) { canvas.width = width; canvas.height = height; + } else { + context.clearRect(0, 0, width, height); } this.renderedTiles.length = 0; @@ -221,10 +219,25 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function(frameState, layer } } - this.renderedTileRange_ = tileRange; this.renderedRevision_ = sourceRevision; + this.renderedResolution_ = tileResolution; + this.renderedExtent_ = imageExtent; } + var scale = pixelRatio / tilePixelRatio * this.renderedResolution_ / viewResolution; + var transform = ol.transform.compose(this.imageTransform_, + pixelRatio * size[0] / 2, pixelRatio * size[1] / 2, + scale, scale, + 0, + tilePixelRatio * (this.renderedExtent_[0] - viewCenter[0]) / this.renderedResolution_, + tilePixelRatio * (viewCenter[1] - this.renderedExtent_[3]) / this.renderedResolution_); + ol.transform.compose(this.coordinateToCanvasPixelTransform_, + pixelRatio * size[0] / 2 - transform[4], pixelRatio * size[1] / 2 - transform[5], + pixelRatio / viewResolution, -pixelRatio / viewResolution, + 0, + -viewCenter[0], -viewCenter[1]); + + this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange); this.manageTilePyramid(frameState, tileSource, tileGrid, pixelRatio, projection, extent, z, tileLayer.getPreload());