From 98b823c5fcd2a9f9971b26490226f8f1da53ed2e Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 25 Dec 2015 22:12:01 +0100 Subject: [PATCH 1/3] Render tiles directly to the map canvas This change removes the overhead of rendering tiles to an intermediate canvas. --- .../canvas/canvastilelayerrenderer.js | 479 ++++++------------ src/ol/source/rastersource.js | 35 +- 2 files changed, 176 insertions(+), 338 deletions(-) diff --git a/src/ol/renderer/canvas/canvastilelayerrenderer.js b/src/ol/renderer/canvas/canvastilelayerrenderer.js index 5ebb1b17e4..816ade549a 100644 --- a/src/ol/renderer/canvas/canvastilelayerrenderer.js +++ b/src/ol/renderer/canvas/canvastilelayerrenderer.js @@ -1,20 +1,17 @@ // FIXME find correct globalCompositeOperation -// FIXME optimize :-) goog.provide('ol.renderer.canvas.TileLayer'); goog.require('goog.asserts'); -goog.require('goog.vec.Mat4'); -goog.require('ol.Size'); goog.require('ol.TileRange'); goog.require('ol.TileState'); goog.require('ol.array'); goog.require('ol.dom'); goog.require('ol.extent'); goog.require('ol.layer.Tile'); +goog.require('ol.render.EventType'); goog.require('ol.renderer.canvas.Layer'); -goog.require('ol.size'); -goog.require('ol.vec.Mat4'); +goog.require('ol.source.Tile'); /** @@ -28,63 +25,15 @@ ol.renderer.canvas.TileLayer = function(tileLayer) { /** * @private - * @type {HTMLCanvasElement} + * @type {Object.>} */ - this.canvas_ = null; - - /** - * @private - * @type {ol.Size} - */ - this.canvasSize_ = null; - - /** - * @private - * @type {boolean} - */ - this.canvasTooBig_ = false; + this.clipExtents_ = null; /** * @private * @type {CanvasRenderingContext2D} */ - this.context_ = null; - - /** - * @private - * @type {!goog.vec.Mat4.Number} - */ - this.imageTransform_ = goog.vec.Mat4.createNumber(); - - /** - * @private - * @type {?goog.vec.Mat4.Number} - */ - this.imageTransformInv_ = null; - - /** - * @private - * @type {number} - */ - this.renderedCanvasZ_ = NaN; - - /** - * @private - * @type {number} - */ - this.renderedTileWidth_ = NaN; - - /** - * @private - * @type {number} - */ - this.renderedTileHeight_ = NaN; - - /** - * @private - * @type {ol.TileRange} - */ - this.renderedCanvasTileRange_ = null; + this.context_ = ol.dom.createCanvasContext2D(); /** * @private @@ -94,9 +43,9 @@ ol.renderer.canvas.TileLayer = function(tileLayer) { /** * @private - * @type {ol.Size} + * @type {ol.Extent} */ - this.tmpSize_ = [0, 0]; + this.tmpExtent_ = ol.extent.createEmpty(); }; goog.inherits(ol.renderer.canvas.TileLayer, ol.renderer.canvas.Layer); @@ -105,86 +54,131 @@ goog.inherits(ol.renderer.canvas.TileLayer, ol.renderer.canvas.Layer); /** * @inheritDoc */ -ol.renderer.canvas.TileLayer.prototype.getImage = function() { - return this.canvas_; +ol.renderer.canvas.TileLayer.prototype.composeFrame = function( + frameState, layerState, context) { + 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 pixelScale = pixelRatio / resolution; + var layer = this.getLayer(); + var source = layer.getSource(); + goog.asserts.assertInstanceof(source, ol.source.Tile, + 'source is an ol.source.Tile'); + var tileGutter = source.getGutter(); + var opaque = source.getOpaque(projection); + + var transform = this.getTransform(frameState, 0); + + this.dispatchPreComposeEvent(context, frameState, transform); + + var renderContext; + if (layer.hasListener(ol.render.EventType.RENDER)) { + // resize and clear + this.context_.canvas.width = context.canvas.width; + this.context_.canvas.height = context.canvas.height; + renderContext = this.context_; + } else { + renderContext = context; + } + var offsetX = Math.round(pixelRatio * size[0] / 2); + var offsetY = Math.round(pixelRatio * size[1] / 2); + // Sub-pixel overlap between tiles to avoid gaps + var overlap = (rotation * 180 / Math.PI) % 90 === 0 ? 0 : + opaque ? 0.25 : 0.125; + + renderContext.translate(offsetX, offsetY); + renderContext.rotate(rotation); + + // 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 tileGrid = source.getTileGridForProjection(projection); + var tilesToDraw = this.renderedTiles_; + + var clipExtents, clipExtent, currentZ, i, ii, j, jj, insertPoint; + var origin, tile, tileExtent, tileHeight, tileOffsetX, tileOffsetY; + var tilePixelSize, tileWidth; + for (i = 0, ii = tilesToDraw.length; i < ii; ++i) { + tile = tilesToDraw[i]; + tileExtent = tileGrid.getTileCoordExtent( + tile.getTileCoord(), this.tmpExtent_); + clipExtents = !opaque && this.clipExtents_[tile.tileCoord.toString()]; + if (clipExtents) { + // Create a clip mask for regions in this low resolution tile that will be + // filled by a higher resolution tile + renderContext.save(); + renderContext.beginPath(); + renderContext.moveTo((tileExtent[0] - center[0]) * pixelScale, + (center[1] - tileExtent[1]) * pixelScale); + renderContext.lineTo((tileExtent[2] - center[0]) * pixelScale, + (center[1] - tileExtent[1]) * pixelScale); + renderContext.lineTo((tileExtent[2] - center[0]) * pixelScale, + (center[1] - tileExtent[3]) * pixelScale); + renderContext.lineTo((tileExtent[0] - center[0]) * pixelScale, + (center[1] - tileExtent[3]) * pixelScale); + renderContext.closePath(); + for (j = 0, jj = clipExtents.length; j < jj; ++j) { + clipExtent = clipExtents[j]; + renderContext.moveTo((clipExtent[0] - center[0]) * pixelScale, + (center[1] - clipExtent[1]) * pixelScale); + renderContext.lineTo((clipExtent[0] - center[0]) * pixelScale, + (center[1] - clipExtent[3]) * pixelScale); + renderContext.lineTo((clipExtent[2] - center[0]) * pixelScale, + (center[1] - clipExtent[3]) * pixelScale); + renderContext.lineTo((clipExtent[2] - center[0]) * pixelScale, + (center[1] - clipExtent[1]) * pixelScale); + renderContext.closePath(); + } + renderContext.clip(); + } + currentZ = tile.getTileCoord()[0]; + tilePixelSize = source.getTilePixelSize(currentZ, pixelRatio, projection); + insertPoint = ol.extent.getTopLeft(tileExtent); + tileWidth = Math.round(ol.extent.getWidth(tileExtent) * pixelScale); + tileHeight = Math.round(ol.extent.getHeight(tileExtent) * pixelScale); + // Calculate all insert points from a common origin and tile widths to avoid + // gaps caused by rounding + origin = ol.extent.getBottomLeft(tileGrid.getTileCoordExtent( + tileGrid.getTileCoordForCoordAndZ(center, currentZ))); + tileOffsetX = Math.round((origin[0] - center[0]) * pixelScale); + tileOffsetY = Math.round((center[1] - origin[1]) * pixelScale); + renderContext.drawImage(tile.getImage(), tileGutter, tileGutter, + tilePixelSize[0], tilePixelSize[1], + Math.round((insertPoint[0] - origin[0]) * pixelScale / tileWidth) * + tileWidth + tileOffsetX - overlap, + Math.round((origin[1] - insertPoint[1]) * pixelScale / tileHeight) * + tileHeight + tileOffsetY - overlap, + tileWidth + 2 * overlap, tileHeight + 2 * overlap); + if (clipExtents) { + renderContext.restore(); + } + } + + renderContext.rotate(-rotation); + renderContext.translate(-offsetX, -offsetY); + + if (renderContext != context) { + this.dispatchRenderEvent(renderContext, frameState, transform); + context.drawImage(renderContext.canvas, 0, 0); + } + renderContext.globalAlpha = alpha; + + this.dispatchPostComposeEvent(context, frameState, transform); }; /** * @inheritDoc */ -ol.renderer.canvas.TileLayer.prototype.getImageTransform = function() { - return this.imageTransform_; -}; - - -/** - * @inheritDoc - */ -ol.renderer.canvas.TileLayer.prototype.prepareFrame = function(frameState, layerState) { - - // - // Warning! You're entering a dangerous zone! - // - // The canvas tile layer renderering is highly optimized, hence - // the complexity of this function. For best performance we try - // to minimize the number of pixels to update on the canvas. This - // includes: - // - // - Only drawing pixels that will be visible. - // - Not re-drawing pixels/tiles that are already correct. - // - Minimizing calls to clearRect. - // - Never shrink the canvas. Just make it bigger when necessary. - // Re-sizing the canvas also clears it, which further means - // re-creating it (expensive). - // - // The various steps performed by this functions: - // - // - Create a canvas element if none has been created yet. - // - // - Make the canvas bigger if it's too small. Note that we never shrink - // the canvas, we just make it bigger when necessary, when rotating for - // example. Note also that the canvas always contains a whole number - // of tiles. - // - // - Invalidate the canvas tile range (renderedCanvasTileRange_ = null) - // if (1) the canvas has been enlarged, or (2) the zoom level changes, - // or (3) the canvas tile range doesn't contain the required tile - // range. This canvas tile range invalidation thing is related to - // an optimization where we attempt to redraw as few pixels as - // possible on each prepareFrame call. - // - // - If the canvas tile range has been invalidated we reset - // renderedCanvasTileRange_ and reset the renderedTiles_ array. - // The renderedTiles_ array is the structure used to determine - // the canvas pixels that need not be redrawn from one prepareFrame - // call to another. It records while tile has been rendered at - // which position in the canvas. - // - // - We then determine the tiles to draw on the canvas. Tiles for - // the target resolution may not be loaded yet. In that case we - // use low-resolution/interim tiles if loaded already. And, if - // for a non-yet-loaded tile we haven't found a corresponding - // low-resolution tile we indicate that the pixels for that - // tile must be cleared on the canvas. Note: determining the - // interim tiles is based on tile extents instead of tile - // coords, this is to be able to handler irregular tile grids. - // - // - We're now ready to render. We start by calling clearRect - // for the tiles that aren't loaded yet and are not fully covered - // by a low-resolution tile (if they're loaded, we'll draw them; - // if they're fully covered by a low-resolution tile then there's - // no need to clear). We then render the tiles "back to front", - // i.e. starting with the low-resolution tiles. - // - // - After rendering some bookkeeping is performed (updateUsedTiles, - // etc.). manageTilePyramid is what enqueue tiles in the tile - // queue for loading. - // - // - The last step involves updating the image transform matrix, - // which will be used by the map renderer for the final - // composition and positioning. - // +ol.renderer.canvas.TileLayer.prototype.prepareFrame = function( + frameState, layerState) { var pixelRatio = frameState.pixelRatio; var viewState = frameState.viewState; @@ -195,14 +189,8 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function(frameState, layer 'layer is an instance of ol.layer.Tile'); var tileSource = tileLayer.getSource(); var tileGrid = tileSource.getTileGridForProjection(projection); - var tileGutter = tileSource.getGutter(); var z = tileGrid.getZForResolution(viewState.resolution); - var tilePixelSize = - tileSource.getTilePixelSize(z, frameState.pixelRatio, projection); - var tilePixelRatio = tilePixelSize[0] / - ol.size.toSize(tileGrid.getTileSize(z), this.tmpSize_)[0]; var tileResolution = tileGrid.getResolution(z); - var tilePixelResolution = tileResolution / tilePixelRatio; var center = viewState.center; var extent; if (tileResolution == viewState.resolution) { @@ -224,87 +212,11 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function(frameState, layer var tileRange = tileGrid.getTileRangeForExtentAndResolution( extent, tileResolution); - var canvasWidth = tilePixelSize[0] * tileRange.getWidth(); - var canvasHeight = tilePixelSize[1] * tileRange.getHeight(); - - var canvas, context; - if (!this.canvas_) { - goog.asserts.assert(!this.canvasSize_, - 'canvasSize is null (because canvas is null)'); - goog.asserts.assert(!this.context_, - 'context is null (because canvas is null)'); - goog.asserts.assert(!this.renderedCanvasTileRange_, - 'renderedCanvasTileRange is null (because canvas is null)'); - context = ol.dom.createCanvasContext2D(canvasWidth, canvasHeight); - this.canvas_ = context.canvas; - this.canvasSize_ = [canvasWidth, canvasHeight]; - this.context_ = context; - this.canvasTooBig_ = - !ol.renderer.canvas.Layer.testCanvasSize(this.canvasSize_); - } else { - goog.asserts.assert(this.canvasSize_, - 'non-null canvasSize (because canvas is not null)'); - goog.asserts.assert(this.context_, - 'non-null context (because canvas is not null)'); - canvas = this.canvas_; - context = this.context_; - if (this.canvasSize_[0] < canvasWidth || - this.canvasSize_[1] < canvasHeight || - this.renderedTileWidth_ !== tilePixelSize[0] || - this.renderedTileHeight_ !== tilePixelSize[1] || - (this.canvasTooBig_ && (this.canvasSize_[0] > canvasWidth || - this.canvasSize_[1] > canvasHeight))) { - // Canvas is too small or tileSize has changed, resize it. - // We never shrink the canvas, unless - // we know that the current canvas size exceeds the maximum size - canvas.width = canvasWidth; - canvas.height = canvasHeight; - this.canvasSize_ = [canvasWidth, canvasHeight]; - this.canvasTooBig_ = - !ol.renderer.canvas.Layer.testCanvasSize(this.canvasSize_); - this.renderedCanvasTileRange_ = null; - } else { - canvasWidth = this.canvasSize_[0]; - canvasHeight = this.canvasSize_[1]; - if (z != this.renderedCanvasZ_ || - !this.renderedCanvasTileRange_.containsTileRange(tileRange)) { - this.renderedCanvasTileRange_ = null; - } - } - } - - var canvasTileRange, canvasTileRangeWidth, minX, minY; - if (!this.renderedCanvasTileRange_) { - canvasTileRangeWidth = canvasWidth / tilePixelSize[0]; - var canvasTileRangeHeight = canvasHeight / tilePixelSize[1]; - minX = tileRange.minX - - Math.floor((canvasTileRangeWidth - tileRange.getWidth()) / 2); - minY = tileRange.minY - - Math.floor((canvasTileRangeHeight - tileRange.getHeight()) / 2); - this.renderedCanvasZ_ = z; - this.renderedTileWidth_ = tilePixelSize[0]; - this.renderedTileHeight_ = tilePixelSize[1]; - this.renderedCanvasTileRange_ = new ol.TileRange( - minX, minX + canvasTileRangeWidth - 1, - minY, minY + canvasTileRangeHeight - 1); - this.renderedTiles_ = - new Array(canvasTileRangeWidth * canvasTileRangeHeight); - canvasTileRange = this.renderedCanvasTileRange_; - } else { - canvasTileRange = this.renderedCanvasTileRange_; - canvasTileRangeWidth = canvasTileRange.getWidth(); - } - - goog.asserts.assert(canvasTileRange.containsTileRange(tileRange), - 'tileRange is contained in canvasTileRange'); - /** * @type {Object.>} */ var tilesToDrawByZ = {}; tilesToDrawByZ[z] = {}; - /** @type {Array.} */ - var tilesToClear = []; var findLoadedTiles = this.createLoadedTileFinder( tileSource, projection, tilesToDrawByZ); @@ -339,9 +251,6 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function(frameState, layer fullyLoaded = tileGrid.forEachTileCoordParentTileRange( tile.tileCoord, findLoadedTiles, null, tmpTileRange, tmpExtent); if (!fullyLoaded) { - // FIXME we do not need to clear the tile if it is fully covered by its - // children - tilesToClear.push(tile); childTileRange = tileGrid.getTileCoordChildTileRange( tile.tileCoord, tmpTileRange, tmpExtent); if (childTileRange) { @@ -352,86 +261,49 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function(frameState, layer } } - var i, ii; - for (i = 0, ii = tilesToClear.length; i < ii; ++i) { - tile = tilesToClear[i]; - x = tilePixelSize[0] * (tile.tileCoord[1] - canvasTileRange.minX); - y = tilePixelSize[1] * (canvasTileRange.maxY - tile.tileCoord[2]); - context.clearRect(x, y, tilePixelSize[0], tilePixelSize[1]); - } - /** @type {Array.} */ var zs = Object.keys(tilesToDrawByZ).map(Number); zs.sort(ol.array.numberSafeCompareFunction); - var opaque = tileSource.getOpaque(projection); - var origin = ol.extent.getTopLeft(tileGrid.getTileCoordExtent( - [z, canvasTileRange.minX, canvasTileRange.maxY], - tmpExtent)); - var currentZ, index, scale, tileCoordKey, tileExtent, tileState, tilesToDraw; - var ix, iy, interimTileRange, maxX, maxY; - var height, width; + var renderables = []; + var i, ii, currentZ, tileCoordKey, tilesToDraw; for (i = 0, ii = zs.length; i < ii; ++i) { currentZ = zs[i]; - tilePixelSize = - tileSource.getTilePixelSize(currentZ, pixelRatio, projection); tilesToDraw = tilesToDrawByZ[currentZ]; - if (currentZ == z) { - for (tileCoordKey in tilesToDraw) { - tile = tilesToDraw[tileCoordKey]; - index = - (tile.tileCoord[2] - canvasTileRange.minY) * canvasTileRangeWidth + - (tile.tileCoord[1] - canvasTileRange.minX); - if (this.renderedTiles_[index] != tile) { - x = tilePixelSize[0] * (tile.tileCoord[1] - canvasTileRange.minX); - y = tilePixelSize[1] * (canvasTileRange.maxY - tile.tileCoord[2]); - tileState = tile.getState(); - if (tileState == ol.TileState.EMPTY || - (tileState == ol.TileState.ERROR && !useInterimTilesOnError) || - !opaque) { - context.clearRect(x, y, tilePixelSize[0], tilePixelSize[1]); - } - if (tileState == ol.TileState.LOADED) { - context.drawImage(tile.getImage(), - tileGutter, tileGutter, tilePixelSize[0], tilePixelSize[1], - x, y, tilePixelSize[0], tilePixelSize[1]); - } - this.renderedTiles_[index] = tile; - } - } - } else { - scale = tileGrid.getResolution(currentZ) / tileResolution; - for (tileCoordKey in tilesToDraw) { - tile = tilesToDraw[tileCoordKey]; - tileExtent = tileGrid.getTileCoordExtent(tile.tileCoord, tmpExtent); - x = (tileExtent[0] - origin[0]) / tilePixelResolution; - y = (origin[1] - tileExtent[3]) / tilePixelResolution; - width = scale * tilePixelSize[0]; - height = scale * tilePixelSize[1]; - tileState = tile.getState(); - if (tileState == ol.TileState.EMPTY || !opaque) { - context.clearRect(x, y, width, height); - } - if (tileState == ol.TileState.LOADED) { - context.drawImage(tile.getImage(), - tileGutter, tileGutter, tilePixelSize[0], tilePixelSize[1], - x, y, width, height); - } - interimTileRange = - tileGrid.getTileRangeForExtentAndZ(tileExtent, z, tmpTileRange); - minX = Math.max(interimTileRange.minX, canvasTileRange.minX); - maxX = Math.min(interimTileRange.maxX, canvasTileRange.maxX); - minY = Math.max(interimTileRange.minY, canvasTileRange.minY); - maxY = Math.min(interimTileRange.maxY, canvasTileRange.maxY); - for (ix = minX; ix <= maxX; ++ix) { - for (iy = minY; iy <= maxY; ++iy) { - index = (iy - canvasTileRange.minY) * canvasTileRangeWidth + - (ix - canvasTileRange.minX); - this.renderedTiles_[index] = undefined; - } - } + for (tileCoordKey in tilesToDraw) { + tile = tilesToDraw[tileCoordKey]; + if (tile.getState() == ol.TileState.LOADED) { + renderables.push(tile); } } } + this.renderedTiles_ = renderables; + if (!tileSource.getOpaque(projection)) { + var clipExtents = {}; + var tileCoord; + for (i = renderables.length - 1; i >= 0; --i) { + tileCoord = renderables[i].getTileCoord(); + tileGrid.forEachTileCoordParentTileRange(tileCoord, + function(z, tileRange) { + var tiles = tilesToDrawByZ[z]; + if (tiles) { + var key, tile; + for (key in tiles) { + tile = tiles[key]; + if (tileRange.contains(tile.getTileCoord()) && + tile.getState() == ol.TileState.LOADED) { + if (!(key in clipExtents)) { + clipExtents[key] = []; + } + clipExtents[key].push(tileGrid.getTileCoordExtent(tileCoord)); + return true; + } + } + } + return false; + }, this, tmpTileRange, tmpExtent); + } + this.clipExtents_ = clipExtents; + } this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange); this.manageTilePyramid(frameState, tileSource, tileGrid, pixelRatio, @@ -439,16 +311,6 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function(frameState, layer this.scheduleExpireCache(frameState, tileSource); this.updateLogos(frameState, tileSource); - ol.vec.Mat4.makeTransform2D(this.imageTransform_, - pixelRatio * frameState.size[0] / 2, - pixelRatio * frameState.size[1] / 2, - pixelRatio * tilePixelResolution / viewState.resolution, - pixelRatio * tilePixelResolution / viewState.resolution, - viewState.rotation, - (origin[0] - center[0]) / tilePixelResolution, - (center[1] - origin[1]) / tilePixelResolution); - this.imageTransformInv_ = null; - return true; }; @@ -456,21 +318,16 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function(frameState, layer /** * @inheritDoc */ -ol.renderer.canvas.TileLayer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) { - if (!this.context_) { - return undefined; - } - - if (!this.imageTransformInv_) { - this.imageTransformInv_ = goog.vec.Mat4.createNumber(); - goog.vec.Mat4.invert(this.imageTransform_, this.imageTransformInv_); - } - - var pixelOnCanvas = - this.getPixelOnCanvas(pixel, this.imageTransformInv_); +ol.renderer.canvas.TileLayer.prototype.forEachLayerAtPixel = function( + pixel, frameState, callback, thisArg) { + var canvas = this.context_.canvas; + var size = frameState.size; + canvas.width = size[0]; + canvas.height = size[1]; + this.composeFrame(frameState, this.getLayer().getLayerState(), this.context_); var imageData = this.context_.getImageData( - pixelOnCanvas[0], pixelOnCanvas[1], 1, 1).data; + pixel[0], pixel[1], 1, 1).data; if (imageData[3] > 0) { return callback.call(thisArg, this.getLayer()); diff --git a/src/ol/source/rastersource.js b/src/ol/source/rastersource.js index ec11a98e30..150e63256d 100644 --- a/src/ol/source/rastersource.js +++ b/src/ol/source/rastersource.js @@ -345,39 +345,20 @@ ol.source.Raster.prototype.onWorkerComplete_ = function(frameState, callback, er */ ol.source.Raster.getImageData_ = function(renderer, frameState, layerState) { renderer.prepareFrame(frameState, layerState); - // We should be able to call renderer.composeFrame(), but this is inefficient - // for tiled sources (we've already rendered to an intermediate canvas in the - // prepareFrame call and we don't need to render again to the output canvas). - // TODO: make all canvas renderers render to a single canvas - var image = renderer.getImage(); - if (!image) { - return null; - } - var imageTransform = renderer.getImageTransform(); - var dx = Math.round(goog.vec.Mat4.getElement(imageTransform, 0, 3)); - var dy = Math.round(goog.vec.Mat4.getElement(imageTransform, 1, 3)); var width = frameState.size[0]; var height = frameState.size[1]; - if (image instanceof Image) { - if (!ol.source.Raster.context_) { + if (!ol.source.Raster.context_) { + ol.source.Raster.context_ = ol.dom.createCanvasContext2D(width, height); + } else { + var canvas = ol.source.Raster.context_.canvas; + if (canvas.width !== width || canvas.height !== height) { ol.source.Raster.context_ = ol.dom.createCanvasContext2D(width, height); } else { - var canvas = ol.source.Raster.context_.canvas; - if (canvas.width !== width || canvas.height !== height) { - ol.source.Raster.context_ = ol.dom.createCanvasContext2D(width, height); - } else { - ol.source.Raster.context_.clearRect(0, 0, width, height); - } + ol.source.Raster.context_.clearRect(0, 0, width, height); } - var dw = Math.round( - image.width * goog.vec.Mat4.getElement(imageTransform, 0, 0)); - var dh = Math.round( - image.height * goog.vec.Mat4.getElement(imageTransform, 1, 1)); - ol.source.Raster.context_.drawImage(image, dx, dy, dw, dh); - return ol.source.Raster.context_.getImageData(0, 0, width, height); - } else { - return image.getContext('2d').getImageData(-dx, -dy, width, height); } + renderer.composeFrame(frameState, layerState, ol.source.Raster.context_); + return ol.source.Raster.context_.getImageData(0, 0, width, height); }; From a109062b1fd7eb7d060b12145f585c632e7c21fe Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sat, 13 Feb 2016 21:55:09 +0100 Subject: [PATCH 2/3] Rotate map canvas after composition --- changelog/upgrade-notes.md | 38 +++++++++++ examples/layer-clipping.js | 10 ++- src/ol/extent.js | 30 +++++---- .../canvas/canvasimagelayerrenderer.js | 3 +- src/ol/renderer/canvas/canvaslayerrenderer.js | 40 ++++++------ src/ol/renderer/canvas/canvasmaprenderer.js | 61 ++++++++++++++++-- .../canvas/canvastilelayerrenderer.js | 51 ++++++--------- .../canvas/canvasvectorlayerrenderer.js | 8 +++ .../canvas/canvasvectortilelayerrenderer.js | 7 +- .../spec/ol/expected/rotate-canvas.png | Bin 615 -> 753 bytes .../ol/layer/expected/vectortile-canvas.png | Bin 5650 -> 5651 bytes test_rendering/spec/ol/map.test.js | 2 +- 12 files changed, 171 insertions(+), 79 deletions(-) diff --git a/changelog/upgrade-notes.md b/changelog/upgrade-notes.md index 387a1797ff..d0f3830f3a 100644 --- a/changelog/upgrade-notes.md +++ b/changelog/upgrade-notes.md @@ -2,6 +2,44 @@ ### v3.14.0 +#### Layer pre-/postcompose event changes + +It is the responsibility of the application to undo any canvas transform changes at the end of a layer 'precompose' or 'postcompose' handler. Previously, it was ok to set a null transform. The API now guarantees a device pixel coordinate system on the canvas with its origin in the top left corner of the map. However, applications should not rely on the underlying canvas being the same size as the visible viewport. + +Old code: +```js +layer.on('precompose', function(e) { + // rely on canvas dimensions to move coordinate origin to center + e.context.translate(e.context.canvas.width / 2, e.context.canvas.height / 2); + e.context.scale(3, 3); + // draw an x in the center of the viewport + e.context.moveTo(-20, -20); + e.context.lineTo(20, 20); + e.context.moveTo(-20, 20); + e.context.lineTo(20, -20); + // rely on the canvas having a null transform + e.context.setTransform(1, 0, 0, 1, 0, 0); +}); +``` +New code: +```js +layer.on('precompose', function(e) { + // use map size and pixel ratio to move coordinate origin to center + var size = map.getSize(); + var pixelRatio = e.frameState.pixelRatio; + e.context.translate(size[0] / 2 * pixelRatio, size[1] / 2 * pixelRatio); + e.context.scale(3, 3); + // draw an x in the center of the viewport + e.context.moveTo(-20, -20); + e.context.lineTo(20, 20); + e.context.moveTo(-20, 20); + e.context.lineTo(20, -20); + // undo all transforms + e.context.scale(1 / 3, 1 / 3); + e.context.translate(-size[0] / 2 * pixelRatio, -size[1] / 2 * pixelRatio); +}); +``` + ### v3.13.0 #### `proj4js` integration diff --git a/examples/layer-clipping.js b/examples/layer-clipping.js index 1a5fa76994..7bf68cd908 100644 --- a/examples/layer-clipping.js +++ b/examples/layer-clipping.js @@ -25,8 +25,10 @@ var map = new ol.Map({ osm.on('precompose', function(event) { var ctx = event.context; ctx.save(); - ctx.translate(ctx.canvas.width / 2, ctx.canvas.height / 2); - ctx.scale(3, 3); + var pixelRatio = event.frameState.pixelRatio; + var size = map.getSize(); + ctx.translate(size[0] / 2 * pixelRatio, size[1] / 2 * pixelRatio); + ctx.scale(3 * pixelRatio, 3 * pixelRatio); ctx.translate(-75, -80); ctx.beginPath(); ctx.moveTo(75, 40); @@ -37,7 +39,9 @@ osm.on('precompose', function(event) { ctx.bezierCurveTo(130, 62.5, 130, 25, 100, 25); ctx.bezierCurveTo(85, 25, 75, 37, 75, 40); ctx.clip(); - ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.translate(75, 80); + ctx.scale(1 / 3 / pixelRatio, 1 / 3 / pixelRatio); + ctx.translate(-size[0] / 2 * pixelRatio, -size[1] / 2 * pixelRatio); }); osm.on('postcompose', function(event) { diff --git a/src/ol/extent.js b/src/ol/extent.js index 2ce48e6fff..19a4c8a617 100644 --- a/src/ol/extent.js +++ b/src/ol/extent.js @@ -575,18 +575,24 @@ ol.extent.getForViewAndSize = function(center, resolution, rotation, size, opt_e var dy = resolution * size[1] / 2; var cosRotation = Math.cos(rotation); var sinRotation = Math.sin(rotation); - /** @type {Array.} */ - var xs = [-dx, -dx, dx, dx]; - /** @type {Array.} */ - var ys = [-dy, dy, -dy, dy]; - var i, x, y; - for (i = 0; i < 4; ++i) { - x = xs[i]; - y = ys[i]; - xs[i] = center[0] + x * cosRotation - y * sinRotation; - ys[i] = center[1] + x * sinRotation + y * cosRotation; - } - return ol.extent.boundingExtentXYs_(xs, ys, opt_extent); + var xCos = dx * cosRotation; + var xSin = dx * sinRotation; + var yCos = dy * cosRotation; + var ySin = dy * sinRotation; + var x = center[0]; + var y = center[1]; + var x0 = x - xCos + ySin; + var x1 = x - xCos - ySin; + var x2 = x + xCos - ySin; + var x3 = x + xCos + ySin; + var y0 = y - xSin - yCos; + var y1 = y - xSin + yCos; + var y2 = y + xSin + yCos; + var y3 = y + xSin - yCos; + return ol.extent.createOrUpdate( + Math.min(x0, x1, x2, x3), Math.min(y0, y1, y2, y3), + Math.max(x0, x1, x2, x3), Math.max(y0, y1, y2, y3), + opt_extent); }; diff --git a/src/ol/renderer/canvas/canvasimagelayerrenderer.js b/src/ol/renderer/canvas/canvasimagelayerrenderer.js index 4598502dc6..9b4aeae64b 100644 --- a/src/ol/renderer/canvas/canvasimagelayerrenderer.js +++ b/src/ol/renderer/canvas/canvasimagelayerrenderer.js @@ -147,7 +147,6 @@ ol.renderer.canvas.ImageLayer.prototype.prepareFrame = function(frameState, laye var viewState = frameState.viewState; var viewCenter = viewState.center; var viewResolution = viewState.resolution; - var viewRotation = viewState.rotation; var image; var imageLayer = this.getLayer(); @@ -195,7 +194,7 @@ ol.renderer.canvas.ImageLayer.prototype.prepareFrame = function(frameState, laye pixelRatio * frameState.size[0] / 2, pixelRatio * frameState.size[1] / 2, scale, scale, - viewRotation, + 0, imagePixelRatio * (imageExtent[0] - viewCenter[0]) / imageResolution, imagePixelRatio * (viewCenter[1] - imageExtent[3]) / imageResolution); this.imageTransformInv_ = null; diff --git a/src/ol/renderer/canvas/canvaslayerrenderer.js b/src/ol/renderer/canvas/canvaslayerrenderer.js index a3502a1d10..6ccde19026 100644 --- a/src/ol/renderer/canvas/canvaslayerrenderer.js +++ b/src/ol/renderer/canvas/canvaslayerrenderer.js @@ -51,6 +51,8 @@ ol.renderer.canvas.Layer.prototype.composeFrame = function(frameState, layerStat goog.asserts.assert(extent !== undefined, 'layerState extent is defined'); var pixelRatio = frameState.pixelRatio; + var width = frameState.size[0] * pixelRatio; + var height = frameState.size[1] * pixelRatio; var topLeft = ol.extent.getTopLeft(extent); var topRight = ol.extent.getTopRight(extent); var bottomRight = ol.extent.getBottomRight(extent); @@ -66,12 +68,18 @@ ol.renderer.canvas.Layer.prototype.composeFrame = function(frameState, layerStat bottomLeft, bottomLeft); context.save(); + context.translate(width / 2, height / 2); + context.rotate(-frameState.viewState.rotation); + context.translate(-width / 2, -height / 2); context.beginPath(); context.moveTo(topLeft[0] * pixelRatio, topLeft[1] * pixelRatio); context.lineTo(topRight[0] * pixelRatio, topRight[1] * pixelRatio); context.lineTo(bottomRight[0] * pixelRatio, bottomRight[1] * pixelRatio); context.lineTo(bottomLeft[0] * pixelRatio, bottomLeft[1] * pixelRatio); context.clip(); + context.translate(width / 2, height / 2); + context.rotate(frameState.viewState.rotation); + context.translate(-width / 2, -height / 2); } var imageTransform = this.getImageTransform(); @@ -83,24 +91,12 @@ ol.renderer.canvas.Layer.prototype.composeFrame = function(frameState, layerStat // for performance reasons, context.setTransform is only used // when the view is rotated. see http://jsperf.com/canvas-transform - if (frameState.viewState.rotation === 0) { - var dx = goog.vec.Mat4.getElement(imageTransform, 0, 3); - var dy = goog.vec.Mat4.getElement(imageTransform, 1, 3); - var dw = image.width * goog.vec.Mat4.getElement(imageTransform, 0, 0); - var dh = image.height * goog.vec.Mat4.getElement(imageTransform, 1, 1); - context.drawImage(image, 0, 0, +image.width, +image.height, - Math.round(dx), Math.round(dy), Math.round(dw), Math.round(dh)); - } else { - context.setTransform( - goog.vec.Mat4.getElement(imageTransform, 0, 0), - goog.vec.Mat4.getElement(imageTransform, 1, 0), - goog.vec.Mat4.getElement(imageTransform, 0, 1), - goog.vec.Mat4.getElement(imageTransform, 1, 1), - goog.vec.Mat4.getElement(imageTransform, 0, 3), - goog.vec.Mat4.getElement(imageTransform, 1, 3)); - context.drawImage(image, 0, 0); - context.setTransform(1, 0, 0, 1, 0, 0); - } + var dx = goog.vec.Mat4.getElement(imageTransform, 0, 3); + var dy = goog.vec.Mat4.getElement(imageTransform, 1, 3); + var dw = image.width * goog.vec.Mat4.getElement(imageTransform, 0, 0); + var dh = image.height * goog.vec.Mat4.getElement(imageTransform, 1, 1); + context.drawImage(image, 0, 0, +image.width, +image.height, + Math.round(dx), Math.round(dy), Math.round(dw), Math.round(dh)); context.globalAlpha = alpha; if (clipped) { @@ -123,6 +119,11 @@ ol.renderer.canvas.Layer.prototype.composeFrame = function(frameState, layerStat ol.renderer.canvas.Layer.prototype.dispatchComposeEvent_ = function(type, context, frameState, opt_transform) { var layer = this.getLayer(); if (layer.hasListener(type)) { + var width = frameState.size[0] * frameState.pixelRatio; + var height = frameState.size[1] * frameState.pixelRatio; + context.translate(width / 2, height / 2); + context.rotate(-frameState.viewState.rotation); + context.translate(-width / 2, -height / 2); var transform = opt_transform !== undefined ? opt_transform : this.getTransform(frameState, 0); var render = new ol.render.canvas.Immediate( @@ -132,6 +133,9 @@ ol.renderer.canvas.Layer.prototype.dispatchComposeEvent_ = function(type, contex context, null); layer.dispatchEvent(composeEvent); render.flush(); + context.translate(width / 2, height / 2); + context.rotate(frameState.viewState.rotation); + context.translate(-width / 2, -height / 2); } }; diff --git a/src/ol/renderer/canvas/canvasmaprenderer.js b/src/ol/renderer/canvas/canvasmaprenderer.js index f446e6a7e7..9dbedbc608 100644 --- a/src/ol/renderer/canvas/canvasmaprenderer.js +++ b/src/ol/renderer/canvas/canvasmaprenderer.js @@ -11,6 +11,7 @@ goog.require('ol.RendererType'); goog.require('ol.array'); goog.require('ol.css'); goog.require('ol.dom'); +goog.require('ol.extent'); goog.require('ol.layer.Image'); goog.require('ol.layer.Layer'); goog.require('ol.layer.Tile'); @@ -45,6 +46,12 @@ ol.renderer.canvas.Map = function(container, map) { */ this.context_ = ol.dom.createCanvasContext2D(); + /** + * @private + * @type {CanvasRenderingContext2D} + */ + this.renderContext_ = ol.dom.createCanvasContext2D(); + /** * @private * @type {HTMLCanvasElement} @@ -56,6 +63,24 @@ ol.renderer.canvas.Map = function(container, map) { this.canvas_.className = ol.css.CLASS_UNSELECTABLE; goog.dom.insertChildAt(container, this.canvas_, 0); + /** + * @private + * @type {HTMLCanvasElement} + */ + this.renderCanvas_ = this.renderContext_.canvas; + + /** + * @private + * @type {ol.Coordinate} + */ + this.pixelCenter_ = [0, 0]; + + /** + * @private + * @type {ol.Extent} + */ + this.pixelExtent_ = ol.extent.createEmpty(); + /** * @private * @type {boolean} @@ -156,14 +181,27 @@ ol.renderer.canvas.Map.prototype.renderFrame = function(frameState) { return; } - var context = this.context_; - var width = frameState.size[0] * frameState.pixelRatio; - var height = frameState.size[1] * frameState.pixelRatio; - if (this.canvas_.width != width || this.canvas_.height != height) { - this.canvas_.width = width; - this.canvas_.height = height; + var context; + var pixelRatio = frameState.pixelRatio; + var width = frameState.size[0] * pixelRatio; + var height = frameState.size[1] * pixelRatio; + this.canvas_.width = width; + this.canvas_.height = height; + + var rotation = frameState.viewState.rotation; + var pixelExtent; + if (rotation) { + context = this.renderContext_; + pixelExtent = ol.extent.getForViewAndSize(this.pixelCenter_, pixelRatio, + rotation, frameState.size, this.pixelExtent_); + var renderWidth = ol.extent.getWidth(pixelExtent); + var renderHeight = ol.extent.getHeight(pixelExtent); + this.renderCanvas_.width = renderWidth + 0.5; + this.renderCanvas_.height = renderHeight + 0.5; + this.renderContext_.translate(Math.round((renderWidth - width) / 2), + Math.round((renderHeight - height) / 2)); } else { - context.clearRect(0, 0, this.canvas_.width, this.canvas_.height); + context = this.context_; } this.calculateMatrices2D(frameState); @@ -190,6 +228,15 @@ ol.renderer.canvas.Map.prototype.renderFrame = function(frameState) { } } + if (rotation) { + this.context_.translate(width / 2, height / 2); + this.context_.rotate(rotation); + this.context_.drawImage(this.renderCanvas_, + Math.round(pixelExtent[0]), Math.round(pixelExtent[1])); + this.context_.rotate(-rotation); + this.context_.translate(-width / 2, -height / 2); + } + this.dispatchComposeEvent_( ol.render.EventType.POSTCOMPOSE, frameState); diff --git a/src/ol/renderer/canvas/canvastilelayerrenderer.js b/src/ol/renderer/canvas/canvastilelayerrenderer.js index 816ade549a..3ebc5ecb7e 100644 --- a/src/ol/renderer/canvas/canvastilelayerrenderer.js +++ b/src/ol/renderer/canvas/canvastilelayerrenderer.js @@ -61,7 +61,6 @@ ol.renderer.canvas.TileLayer.prototype.composeFrame = function( var center = viewState.center; var projection = viewState.projection; var resolution = viewState.resolution; - var rotation = viewState.rotation; var size = frameState.size; var pixelScale = pixelRatio / resolution; var layer = this.getLayer(); @@ -86,12 +85,6 @@ ol.renderer.canvas.TileLayer.prototype.composeFrame = function( } var offsetX = Math.round(pixelRatio * size[0] / 2); var offsetY = Math.round(pixelRatio * size[1] / 2); - // Sub-pixel overlap between tiles to avoid gaps - var overlap = (rotation * 180 / Math.PI) % 90 === 0 ? 0 : - opaque ? 0.25 : 0.125; - - renderContext.translate(offsetX, offsetY); - renderContext.rotate(rotation); // for performance reasons, context.save / context.restore is not used // to save and restore the transformation matrix and the opacity. @@ -115,25 +108,25 @@ ol.renderer.canvas.TileLayer.prototype.composeFrame = function( // filled by a higher resolution tile renderContext.save(); renderContext.beginPath(); - renderContext.moveTo((tileExtent[0] - center[0]) * pixelScale, - (center[1] - tileExtent[1]) * pixelScale); - renderContext.lineTo((tileExtent[2] - center[0]) * pixelScale, - (center[1] - tileExtent[1]) * pixelScale); - renderContext.lineTo((tileExtent[2] - center[0]) * pixelScale, - (center[1] - tileExtent[3]) * pixelScale); - renderContext.lineTo((tileExtent[0] - center[0]) * pixelScale, - (center[1] - tileExtent[3]) * pixelScale); + renderContext.moveTo((tileExtent[0] - center[0]) * pixelScale + offsetX, + (center[1] - tileExtent[1]) * pixelScale + offsetY); + renderContext.lineTo((tileExtent[2] - center[0]) * pixelScale + offsetX, + (center[1] - tileExtent[1]) * pixelScale + offsetY); + renderContext.lineTo((tileExtent[2] - center[0]) * pixelScale + offsetX, + (center[1] - tileExtent[3]) * pixelScale + offsetY); + renderContext.lineTo((tileExtent[0] - center[0]) * pixelScale + offsetX, + (center[1] - tileExtent[3]) * pixelScale + offsetY); renderContext.closePath(); for (j = 0, jj = clipExtents.length; j < jj; ++j) { clipExtent = clipExtents[j]; - renderContext.moveTo((clipExtent[0] - center[0]) * pixelScale, - (center[1] - clipExtent[1]) * pixelScale); - renderContext.lineTo((clipExtent[0] - center[0]) * pixelScale, - (center[1] - clipExtent[3]) * pixelScale); - renderContext.lineTo((clipExtent[2] - center[0]) * pixelScale, - (center[1] - clipExtent[3]) * pixelScale); - renderContext.lineTo((clipExtent[2] - center[0]) * pixelScale, - (center[1] - clipExtent[1]) * pixelScale); + renderContext.moveTo((clipExtent[0] - center[0]) * pixelScale + offsetX, + (center[1] - clipExtent[1]) * pixelScale + offsetY); + renderContext.lineTo((clipExtent[0] - center[0]) * pixelScale + offsetX, + (center[1] - clipExtent[3]) * pixelScale + offsetY); + renderContext.lineTo((clipExtent[2] - center[0]) * pixelScale + offsetX, + (center[1] - clipExtent[3]) * pixelScale + offsetY); + renderContext.lineTo((clipExtent[2] - center[0]) * pixelScale + offsetX, + (center[1] - clipExtent[1]) * pixelScale + offsetY); renderContext.closePath(); } renderContext.clip(); @@ -147,23 +140,19 @@ ol.renderer.canvas.TileLayer.prototype.composeFrame = function( // gaps caused by rounding origin = ol.extent.getBottomLeft(tileGrid.getTileCoordExtent( tileGrid.getTileCoordForCoordAndZ(center, currentZ))); - tileOffsetX = Math.round((origin[0] - center[0]) * pixelScale); - tileOffsetY = Math.round((center[1] - origin[1]) * pixelScale); + tileOffsetX = offsetX + Math.round((origin[0] - center[0]) * pixelScale); + tileOffsetY = offsetY + Math.round((center[1] - origin[1]) * pixelScale); renderContext.drawImage(tile.getImage(), tileGutter, tileGutter, tilePixelSize[0], tilePixelSize[1], Math.round((insertPoint[0] - origin[0]) * pixelScale / tileWidth) * - tileWidth + tileOffsetX - overlap, + tileWidth + tileOffsetX, Math.round((origin[1] - insertPoint[1]) * pixelScale / tileHeight) * - tileHeight + tileOffsetY - overlap, - tileWidth + 2 * overlap, tileHeight + 2 * overlap); + tileHeight + tileOffsetY, tileWidth, tileHeight); if (clipExtents) { renderContext.restore(); } } - renderContext.rotate(-rotation); - renderContext.translate(-offsetX, -offsetY); - if (renderContext != context) { this.dispatchRenderEvent(renderContext, frameState, transform); context.drawImage(renderContext.canvas, 0, 0); diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index 085f3c0717..b3361be8b7 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -106,6 +106,11 @@ ol.renderer.canvas.VectorLayer.prototype.composeFrame = function(frameState, lay var alpha = replayContext.globalAlpha; replayContext.globalAlpha = layerState.opacity; + var width = frameState.size[0] * pixelRatio; + var height = frameState.size[1] * pixelRatio; + replayContext.translate(width / 2, height / 2); + replayContext.rotate(-rotation); + replayContext.translate(-width / 2, -height / 2); replayGroup.replay(replayContext, pixelRatio, transform, rotation, skippedFeatureUids); if (vectorSource.getWrapX() && projection.canWrapX() && @@ -135,6 +140,9 @@ ol.renderer.canvas.VectorLayer.prototype.composeFrame = function(frameState, lay // restore original transform for render and compose events transform = this.getTransform(frameState, 0); } + replayContext.translate(width / 2, height / 2); + replayContext.rotate(rotation); + replayContext.translate(-width / 2, -height / 2); if (replayContext != context) { this.dispatchRenderEvent(replayContext, frameState, transform); diff --git a/src/ol/renderer/canvas/canvasvectortilelayerrenderer.js b/src/ol/renderer/canvas/canvasvectortilelayerrenderer.js index 2a22e9fc4e..80ed48d8a2 100644 --- a/src/ol/renderer/canvas/canvasvectortilelayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectortilelayerrenderer.js @@ -183,12 +183,9 @@ ol.renderer.canvas.VectorTileLayer.prototype.composeFrame = function(frameState, 0, 0, pixelScale, -pixelScale, 0, -center[0], -center[1]); insertPoint = ol.geom.flat.transform.transform2D( ol.extent.getTopLeft(tileExtent), 0, 1, 2, insertTransform); - replayContext.translate(offsetX, offsetY); - replayContext.rotate(rotation); replayContext.drawImage(tileContext.canvas, - Math.round(insertPoint[0]), Math.round(insertPoint[1])); - replayContext.rotate(-rotation); - replayContext.translate(-offsetX, -offsetY); + Math.round(insertPoint[0] + offsetX), + Math.round(insertPoint[1]) + offsetY); } } diff --git a/test_rendering/spec/ol/expected/rotate-canvas.png b/test_rendering/spec/ol/expected/rotate-canvas.png index e614cf3c0858d3a802822c2595545b11c696150d..b46061ae74692dc153b70a1d81bda6b188821eba 100644 GIT binary patch delta 729 zcmV;~0w(?E1n~usB!9R`L_t(&f$i2`NK;W9!10@km5G+%iwKz*2BWp@x>jSNVSlg) zO^B%MsoX3ODl?@(?o&xKg{j-_Hn%zdwWX*jrihwo%kBP~Zo^E>P=;xQG$qF1a(bzV zw#>I{bUyHV|Nb~|&Os0ef}pJfI}i*+WT=UBp241g;^ZNrk$+A^u+b<4!y>q@+Y(;G zXsk$0p;A3nm}R1$N)6Q5DT}LKtRt<4#Y!MJICI_*W=S#)$OPF^Qh)%x#_?R`ktU z%LeCLOJ2@d^V;37Pd86BiE%EczcPdS|<2M#h?4V^83^IV^#eIhbu`ltnMzbvT#&72ua8e$0B1y;)EARJpCw zT>{*ZpM;Y!Tz7}yHF#sC25PWk=)Gm`^Vd7!7GVL9lYc&cc}^2Upa$kjnrbqR>+TR@ zor`zE%&6}U%(c#Z+9(aTNPq>3rl}R>xi)v7P)l6qVYX6WZ}f3JT~T253_tg- zJ_@ox@%w260gzXYe6VDhm@&TEk(s9I-^q1%h~ygkZjpw%AZuhC4~G{_fgoG_B@n#D z0w52vtbgL;dZ46{Uar2>;q4adsoKNp@qJu3hlgPi>2(uH5ttqy(|qw(n#3lMO_sU<cu(lVHTUV!=6-vi7{j4C>!+$Xr$n7)G?e&^kH zmDgW?YyAJmn0PMJcmb_FL$ODUXP&vp&(16`KTxwzj*lJe34a)9J^09S{ms`23=9l` zXVXnK(9$y$c?6W4SXdaQtPazzM)w5F(Yf1i`<{6GN$*U$iTt4Ti1Caw8}uc3c;@=6 zufpZXN1uLj2CcX<;V&bz;e|BA3$*eKMKNMI=j<_KDc-k}Jykd3bD+trvortx`|o%m z)AYcg^oZH4Gk=H8WcWS|d5;*+JhMQYo6US{v~EAHkY0H4h1%qkPmBMuF$-QuH2g3q zW5i(kc?D*szxM;xMHf`t%N<8|Bq)ExufLxE`RCt5XVXnUr5DWrMR^3|DsV-}^nbpU zEdS;bYuUZ9(hC$53og6}`ug+l*WYZ+wzP~5kUJ>$2!F^B@Ovx&$)`mW;*-34uiyrK2%ImtZErlF=9; zA+YG8bVoEs=n~8%STY(TBm@>+l#a#-U4oeeOGaaagutSU($N^9OE8mQ$!Ls_5Lk3k cI&5MD0I=SoGlY$iq5uE@07*qoM6N<$g2yW!-v9sr diff --git a/test_rendering/spec/ol/layer/expected/vectortile-canvas.png b/test_rendering/spec/ol/layer/expected/vectortile-canvas.png index 8b6ddf8536ec9a9b0757718c838ce1639c797340..8c34f9259c789ff5604abe3a17900bf4370df9e8 100644 GIT binary patch literal 5651 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 ZhK7Oy_NV;Rfx})9Lf=%cT-W*e{{i4fF_HiP literal 5650 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 YAU}Y2Zw^?3JL?5lU9z>PHNW-n|6FV;R{#J2 diff --git a/test_rendering/spec/ol/map.test.js b/test_rendering/spec/ol/map.test.js index 424de13691..d08175f855 100644 --- a/test_rendering/spec/ol/map.test.js +++ b/test_rendering/spec/ol/map.test.js @@ -78,7 +78,7 @@ describe('ol.rendering.Map', function() { map.getView().setRotation(90); map.getView().setCenter([10, 10]); expectResemble( - map, 'spec/ol/expected/rotate-canvas.png', IMAGE_TOLERANCE, done); + map, 'spec/ol/expected/rotate-canvas.png', 2.8, done); }); it('tests the WebGL renderer', function(done) { From 952b99742e8b2f67d88a441376675f9bcf3ddc99 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Mon, 15 Feb 2016 01:35:20 +0100 Subject: [PATCH 3/3] Add tests, remove unused code, encapsulate repeated code in functions --- src/ol/render/canvas/canvas.js | 15 ++++ src/ol/renderer/canvas/canvaslayerrenderer.js | 64 ++--------------- .../canvas/canvasvectorlayerrenderer.js | 11 ++- test/spec/ol/render/canvas.test.js | 25 +++++++ .../canvas/canvaslayerrenderer.test.js | 68 +++++++++++++++++++ 5 files changed, 120 insertions(+), 63 deletions(-) create mode 100644 test/spec/ol/render/canvas.test.js create mode 100644 test/spec/ol/renderer/canvas/canvaslayerrenderer.test.js diff --git a/src/ol/render/canvas/canvas.js b/src/ol/render/canvas/canvas.js index 21e546d579..be3d39a949 100644 --- a/src/ol/render/canvas/canvas.js +++ b/src/ol/render/canvas/canvas.js @@ -94,3 +94,18 @@ ol.render.canvas.defaultTextBaseline = 'middle'; * @type {number} */ ol.render.canvas.defaultLineWidth = 1; + + +/** + * @param {CanvasRenderingContext2D} context Context. + * @param {number} rotation Rotation. + * @param {number} offsetX X offset. + * @param {number} offsetY Y offset. + */ +ol.render.canvas.rotateAtOffset = function(context, rotation, offsetX, offsetY) { + if (rotation !== 0) { + context.translate(offsetX, offsetY); + context.rotate(rotation); + context.translate(-offsetX, -offsetY); + } +}; diff --git a/src/ol/renderer/canvas/canvaslayerrenderer.js b/src/ol/renderer/canvas/canvaslayerrenderer.js index 6ccde19026..e34f430fcf 100644 --- a/src/ol/renderer/canvas/canvaslayerrenderer.js +++ b/src/ol/renderer/canvas/canvaslayerrenderer.js @@ -2,12 +2,11 @@ goog.provide('ol.renderer.canvas.Layer'); goog.require('goog.asserts'); goog.require('goog.vec.Mat4'); -goog.require('ol.array'); -goog.require('ol.dom'); goog.require('ol.extent'); goog.require('ol.layer.Layer'); goog.require('ol.render.Event'); goog.require('ol.render.EventType'); +goog.require('ol.render.canvas'); goog.require('ol.render.canvas.Immediate'); goog.require('ol.renderer.Layer'); goog.require('ol.vec.Mat4'); @@ -53,6 +52,7 @@ ol.renderer.canvas.Layer.prototype.composeFrame = function(frameState, layerStat var pixelRatio = frameState.pixelRatio; var width = frameState.size[0] * pixelRatio; var height = frameState.size[1] * pixelRatio; + var rotation = frameState.viewState.rotation; var topLeft = ol.extent.getTopLeft(extent); var topRight = ol.extent.getTopRight(extent); var bottomRight = ol.extent.getBottomRight(extent); @@ -68,18 +68,14 @@ ol.renderer.canvas.Layer.prototype.composeFrame = function(frameState, layerStat bottomLeft, bottomLeft); context.save(); - context.translate(width / 2, height / 2); - context.rotate(-frameState.viewState.rotation); - context.translate(-width / 2, -height / 2); + ol.render.canvas.rotateAtOffset(context, -rotation, width / 2, height / 2); context.beginPath(); context.moveTo(topLeft[0] * pixelRatio, topLeft[1] * pixelRatio); context.lineTo(topRight[0] * pixelRatio, topRight[1] * pixelRatio); context.lineTo(bottomRight[0] * pixelRatio, bottomRight[1] * pixelRatio); context.lineTo(bottomLeft[0] * pixelRatio, bottomLeft[1] * pixelRatio); context.clip(); - context.translate(width / 2, height / 2); - context.rotate(frameState.viewState.rotation); - context.translate(-width / 2, -height / 2); + ol.render.canvas.rotateAtOffset(context, rotation, width / 2, height / 2); } var imageTransform = this.getImageTransform(); @@ -121,9 +117,8 @@ ol.renderer.canvas.Layer.prototype.dispatchComposeEvent_ = function(type, contex if (layer.hasListener(type)) { var width = frameState.size[0] * frameState.pixelRatio; var height = frameState.size[1] * frameState.pixelRatio; - context.translate(width / 2, height / 2); - context.rotate(-frameState.viewState.rotation); - context.translate(-width / 2, -height / 2); + var rotation = frameState.viewState.rotation; + ol.render.canvas.rotateAtOffset(context, -rotation, width / 2, height / 2); var transform = opt_transform !== undefined ? opt_transform : this.getTransform(frameState, 0); var render = new ol.render.canvas.Immediate( @@ -133,9 +128,7 @@ ol.renderer.canvas.Layer.prototype.dispatchComposeEvent_ = function(type, contex context, null); layer.dispatchEvent(composeEvent); render.flush(); - context.translate(width / 2, height / 2); - context.rotate(frameState.viewState.rotation); - context.translate(-width / 2, -height / 2); + ol.render.canvas.rotateAtOffset(context, rotation, width / 2, height / 2); } }; @@ -228,46 +221,3 @@ ol.renderer.canvas.Layer.prototype.getPixelOnCanvas = function(pixelOnMap, image ol.vec.Mat4.multVec2(imageTransformInv, pixelOnMap, pixelOnCanvas); return pixelOnCanvas; }; - - -/** - * @param {ol.Size} size Size. - * @return {boolean} True when the canvas with the current size does not exceed - * the maximum dimensions. - */ -ol.renderer.canvas.Layer.testCanvasSize = (function() { - - /** - * @type {CanvasRenderingContext2D} - */ - var context = null; - - /** - * @type {ImageData} - */ - var imageData = null; - - return function(size) { - if (!context) { - context = ol.dom.createCanvasContext2D(1, 1); - imageData = context.createImageData(1, 1); - var data = imageData.data; - data[0] = 42; - data[1] = 84; - data[2] = 126; - data[3] = 255; - } - var canvas = context.canvas; - var good = size[0] <= canvas.width && size[1] <= canvas.height; - if (!good) { - canvas.width = size[0]; - canvas.height = size[1]; - var x = size[0] - 1; - var y = size[1] - 1; - context.putImageData(imageData, x, y); - var result = context.getImageData(x, y, 1, 1); - good = ol.array.equals(imageData.data, result.data); - } - return good; - }; -})(); diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index b3361be8b7..7599124d24 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -7,6 +7,7 @@ goog.require('ol.dom'); goog.require('ol.extent'); goog.require('ol.layer.Vector'); goog.require('ol.render.EventType'); +goog.require('ol.render.canvas'); goog.require('ol.render.canvas.ReplayGroup'); goog.require('ol.renderer.canvas.Layer'); goog.require('ol.renderer.vector'); @@ -108,9 +109,8 @@ ol.renderer.canvas.VectorLayer.prototype.composeFrame = function(frameState, lay var width = frameState.size[0] * pixelRatio; var height = frameState.size[1] * pixelRatio; - replayContext.translate(width / 2, height / 2); - replayContext.rotate(-rotation); - replayContext.translate(-width / 2, -height / 2); + ol.render.canvas.rotateAtOffset(replayContext, -rotation, + width / 2, height / 2); replayGroup.replay(replayContext, pixelRatio, transform, rotation, skippedFeatureUids); if (vectorSource.getWrapX() && projection.canWrapX() && @@ -140,9 +140,8 @@ ol.renderer.canvas.VectorLayer.prototype.composeFrame = function(frameState, lay // restore original transform for render and compose events transform = this.getTransform(frameState, 0); } - replayContext.translate(width / 2, height / 2); - replayContext.rotate(rotation); - replayContext.translate(-width / 2, -height / 2); + ol.render.canvas.rotateAtOffset(replayContext, rotation, + width / 2, height / 2); if (replayContext != context) { this.dispatchRenderEvent(replayContext, frameState, transform); diff --git a/test/spec/ol/render/canvas.test.js b/test/spec/ol/render/canvas.test.js new file mode 100644 index 0000000000..f39f6a7e53 --- /dev/null +++ b/test/spec/ol/render/canvas.test.js @@ -0,0 +1,25 @@ +goog.provide('ol.test.render.canvas'); + + +describe('ol.render.canvas', function() { + + describe('rotateAtOffset', function() { + it('rotates a canvas at an offset point', function() { + var context = { + translate: sinon.spy(), + rotate: sinon.spy() + }; + ol.render.canvas.rotateAtOffset(context, Math.PI, 10, 10); + expect(context.translate.callCount).to.be(2); + expect(context.translate.firstCall.args).to.eql([10, 10]); + expect(context.translate.secondCall.args).to.eql([-10, -10]); + expect(context.rotate.callCount).to.be(1); + expect(context.rotate.firstCall.args).to.eql([Math.PI]); + }); + }); + +}); + + +goog.require('ol.render'); +goog.require('ol.render.canvas'); diff --git a/test/spec/ol/renderer/canvas/canvaslayerrenderer.test.js b/test/spec/ol/renderer/canvas/canvaslayerrenderer.test.js new file mode 100644 index 0000000000..f06d144002 --- /dev/null +++ b/test/spec/ol/renderer/canvas/canvaslayerrenderer.test.js @@ -0,0 +1,68 @@ +goog.provide('ol.test.renderer.canvas.Layer'); + + +describe('ol.renderer.canvas.Layer', function() { + + describe('#composeFrame()', function() { + it('clips to layer extent and draws image', function() { + var layer = new ol.layer.Image({ + extent: [1, 2, 3, 4] + }); + var renderer = new ol.renderer.canvas.Layer(layer); + var image = new Image(); + image.width = 3; + image.height = 3; + renderer.getImage = function() { + return image; + }; + var frameState = { + viewState: { + center: [2, 3], + resolution: 1, + rotation: 0 + }, + size: [10, 10], + pixelRatio: 1, + coordinateToPixelMatrix: goog.vec.Mat4.createNumber(), + pixelToCoordinateMatrix: goog.vec.Mat4.createNumber() + }; + renderer.getImageTransform = function() { + return goog.vec.Mat4.createNumberIdentity(); + } + ol.renderer.Map.prototype.calculateMatrices2D(frameState); + var layerState = layer.getLayerState(); + var context = { + save: sinon.spy(), + restore: sinon.spy(), + translate: sinon.spy(), + rotate: sinon.spy(), + beginPath: sinon.spy(), + moveTo: sinon.spy(), + lineTo: sinon.spy(), + clip: sinon.spy(), + drawImage: sinon.spy() + } + renderer.composeFrame(frameState, layerState, context); + expect(context.save.callCount).to.be(1); + expect(context.translate.callCount).to.be(0); + expect(context.rotate.callCount).to.be(0); + expect(context.beginPath.callCount).to.be(1); + expect(context.moveTo.firstCall.args).to.eql([4, 4]); + expect(context.lineTo.firstCall.args).to.eql([6, 4]); + expect(context.lineTo.secondCall.args).to.eql([6, 6]); + expect(context.lineTo.thirdCall.args).to.eql([4, 6]); + expect(context.clip.callCount).to.be(1); + expect(context.drawImage.firstCall.args).to.eql( + [renderer.getImage(), 0, 0, 3, 3, 0, 0, 3, 3]); + expect(context.restore.callCount).to.be(1); + }); + }); + +}); + + +goog.require('ol.render.canvas'); +goog.require('goog.vec.Mat4'); +goog.require('ol.layer.Image'); +goog.require('ol.renderer.Map'); +goog.require('ol.renderer.canvas.Layer');