From d854222c4ba5c486100982197e499f2b56cf238b Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Wed, 7 Dec 2016 19:34:28 +0100 Subject: [PATCH] Fix forEachLayerAtPixel and improve class hierarchy --- src/ol/renderer/canvas/imagelayer.js | 105 ++---------- src/ol/renderer/canvas/intermediatecanvas.js | 149 ++++++++++++++++++ src/ol/renderer/canvas/layer.js | 115 ++++---------- src/ol/renderer/canvas/tilelayer.js | 55 ++----- ...yer.test.js => intermediatecanvas.test.js} | 6 +- 5 files changed, 208 insertions(+), 222 deletions(-) create mode 100644 src/ol/renderer/canvas/intermediatecanvas.js rename test/spec/ol/renderer/canvas/{layer.test.js => intermediatecanvas.test.js} (91%) diff --git a/src/ol/renderer/canvas/imagelayer.js b/src/ol/renderer/canvas/imagelayer.js index 6dbf8d4abd..063cf3dd62 100644 --- a/src/ol/renderer/canvas/imagelayer.js +++ b/src/ol/renderer/canvas/imagelayer.js @@ -2,23 +2,20 @@ goog.provide('ol.renderer.canvas.ImageLayer'); goog.require('ol'); goog.require('ol.View'); -goog.require('ol.dom'); goog.require('ol.extent'); -goog.require('ol.functions'); goog.require('ol.proj'); -goog.require('ol.renderer.canvas.Layer'); -goog.require('ol.source.ImageVector'); +goog.require('ol.renderer.canvas.IntermediateCanvas'); goog.require('ol.transform'); /** * @constructor - * @extends {ol.renderer.canvas.Layer} + * @extends {ol.renderer.canvas.IntermediateCanvas} * @param {ol.layer.Image} imageLayer Single image layer. */ ol.renderer.canvas.ImageLayer = function(imageLayer) { - ol.renderer.canvas.Layer.call(this, imageLayer); + ol.renderer.canvas.IntermediateCanvas.call(this, imageLayer); /** * @private @@ -32,88 +29,8 @@ ol.renderer.canvas.ImageLayer = function(imageLayer) { */ this.imageTransform_ = ol.transform.create(); - /** - * @private - * @type {ol.Transform} - */ - this.coordinateToCanvasPixelTransform_ = ol.transform.create(); - - /** - * @private - * @type {CanvasRenderingContext2D} - */ - this.hitCanvasContext_ = null; - -}; -ol.inherits(ol.renderer.canvas.ImageLayer, ol.renderer.canvas.Layer); - - -/** - * @inheritDoc - */ -ol.renderer.canvas.ImageLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) { - var layer = this.getLayer(); - var source = layer.getSource(); - var resolution = frameState.viewState.resolution; - var rotation = frameState.viewState.rotation; - var skippedFeatureUids = frameState.skippedFeatureUids; - return source.forEachFeatureAtCoordinate( - coordinate, resolution, rotation, skippedFeatureUids, - /** - * @param {ol.Feature|ol.render.Feature} feature Feature. - * @return {?} Callback result. - */ - function(feature) { - return callback.call(thisArg, feature, layer); - }); -}; - - -/** - * @param {ol.Coordinate} coordinate Coordinate. - * @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.canvas.ImageLayer.prototype.forEachLayerAtCoordinate = function(coordinate, frameState, callback, thisArg) { - if (!this.getImage()) { - return undefined; - } - - 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 hasFeature = this.forEachFeatureAtCoordinate( - coordinate, frameState, ol.functions.TRUE, this); - - if (hasFeature) { - return callback.call(thisArg, this.getLayer(), null); - } else { - return undefined; - } - } else { - var pixelOnCanvas = ol.transform.apply( - this.coordinateToCanvasPixelTransform_, coordinate.slice()); - - if (!this.hitCanvasContext_) { - this.hitCanvasContext_ = ol.dom.createCanvasContext2D(1, 1); - } - - this.hitCanvasContext_.clearRect(0, 0, 1, 1); - this.hitCanvasContext_.drawImage( - this.getImage(), pixelOnCanvas[0], pixelOnCanvas[1], 1, 1, 0, 0, 1, 1); - - var imageData = this.hitCanvasContext_.getImageData(0, 0, 1, 1).data; - if (imageData[3] > 0) { - return callback.call(thisArg, this.getLayer(), imageData); - } else { - return undefined; - } - } }; +ol.inherits(ol.renderer.canvas.ImageLayer, ol.renderer.canvas.IntermediateCanvas); /** @@ -172,6 +89,7 @@ ol.renderer.canvas.ImageLayer.prototype.prepareFrame = function(frameState, laye var loaded = this.loadImage(image); if (loaded) { this.image_ = image; + this.renderedResolution = viewResolution; } } } @@ -183,19 +101,18 @@ ol.renderer.canvas.ImageLayer.prototype.prepareFrame = function(frameState, laye var imagePixelRatio = image.getPixelRatio(); var scale = pixelRatio * imageResolution / (viewResolution * imagePixelRatio); - var transform = ol.transform.reset(this.imageTransform_); - ol.transform.translate(transform, - pixelRatio * frameState.size[0] / 2, - pixelRatio * frameState.size[1] / 2); - ol.transform.scale(transform, scale, scale); - ol.transform.translate(transform, + var transform = ol.transform.compose(this.imageTransform_, + pixelRatio * size[0] / 2, pixelRatio * size[1] / 2, + scale, scale, + 0, imagePixelRatio * (imageExtent[0] - viewCenter[0]) / imageResolution, imagePixelRatio * (viewCenter[1] - imageExtent[3]) / imageResolution); - 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, -viewCenter[0], -viewCenter[1]); + this.updateAttributions(frameState.attributions, image.getAttributions()); this.updateLogos(frameState, imageSource); } diff --git a/src/ol/renderer/canvas/intermediatecanvas.js b/src/ol/renderer/canvas/intermediatecanvas.js new file mode 100644 index 0000000000..fd8e8d0447 --- /dev/null +++ b/src/ol/renderer/canvas/intermediatecanvas.js @@ -0,0 +1,149 @@ +goog.provide('ol.renderer.canvas.IntermediateCanvas'); + +goog.require('ol'); +goog.require('ol.coordinate'); +goog.require('ol.dom'); +goog.require('ol.renderer.canvas.Layer'); +goog.require('ol.transform'); + + +/** + * @constructor + * @extends {ol.renderer.canvas.Layer} + * @param {ol.layer.Layer} layer Layer. + */ +ol.renderer.canvas.IntermediateCanvas = function(layer) { + + ol.renderer.canvas.Layer.call(this, layer); + + /** + * @protected + * @type {ol.Transform} + */ + this.coordinateToCanvasPixelTransform = ol.transform.create(); + + /** + * @private + * @type {CanvasRenderingContext2D} + */ + this.hitCanvasContext_ = null; + + /** + * @protected + * @type {number} + */ + this.renderedResolution; + +}; +ol.inherits(ol.renderer.canvas.IntermediateCanvas, ol.renderer.canvas.Layer); + + +/** + * @inheritDoc + */ +ol.renderer.canvas.IntermediateCanvas.prototype.composeFrame = function(frameState, layerState, context) { + + this.preCompose(context, frameState); + + var image = this.getImage(); + if (image) { + + // 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 imageTransform = this.getImageTransform(); + // 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 = context.globalAlpha; + context.globalAlpha = layerState.opacity; + + // for performance reasons, context.setTransform is only used + // when the view is rotated. see http://jsperf.com/canvas-transform + var dx = imageTransform[4]; + var dy = imageTransform[5]; + var dw = image.width * imageTransform[0]; + var dh = image.height * imageTransform[3]; + 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) { + context.restore(); + } + } + + this.postCompose(context, frameState, layerState); +}; + + +/** + * @abstract + * @return {HTMLCanvasElement|HTMLVideoElement|Image} Canvas. + */ +ol.renderer.canvas.IntermediateCanvas.prototype.getImage = function() {}; + + +/** + * @abstract + * @return {!ol.Transform} Image transform. + */ +ol.renderer.canvas.IntermediateCanvas.prototype.getImageTransform = function() {}; + + +/** + * @inheritDoc + */ +ol.renderer.canvas.IntermediateCanvas.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) { + var layer = this.getLayer(); + var source = layer.getSource(); + var resolution = frameState.viewState.resolution; + var rotation = frameState.viewState.rotation; + var skippedFeatureUids = frameState.skippedFeatureUids; + return source.forEachFeatureAtCoordinate( + coordinate, resolution, rotation, skippedFeatureUids, + /** + * @param {ol.Feature|ol.render.Feature} feature Feature. + * @return {?} Callback result. + */ + function(feature) { + return callback.call(thisArg, feature, layer); + }); +}; + + +/** + * @inheritDoc + */ +ol.renderer.canvas.IntermediateCanvas.prototype.forEachLayerAtCoordinate = function(coordinate, frameState, callback, thisArg) { + if (!this.getImage()) { + return undefined; + } + + if (this.getLayer().getSource().forEachFeatureAtCoordinate !== ol.nullFunction) { + // for ImageVector sources use the original hit-detection logic, + // so that for example also transparent polygons are detected + return ol.renderer.canvas.Layer.prototype.forEachLayerAtCoordinate.apply(this, arguments); + } else { + var pixel = ol.transform.apply(this.coordinateToCanvasPixelTransform, coordinate); + ol.coordinate.scale(pixel, frameState.viewState.resolution / this.renderedResolution); + + if (!this.hitCanvasContext_) { + this.hitCanvasContext_ = ol.dom.createCanvasContext2D(1, 1); + } + + this.hitCanvasContext_.clearRect(0, 0, 1, 1); + this.hitCanvasContext_.drawImage(this.getImage(), pixel[0], pixel[1], 1, 1, 0, 0, 1, 1); + + var imageData = this.hitCanvasContext_.getImageData(0, 0, 1, 1).data; + if (imageData[3] > 0) { + return callback.call(thisArg, this.getLayer(), imageData); + } else { + return undefined; + } + } +}; diff --git a/src/ol/renderer/canvas/layer.js b/src/ol/renderer/canvas/layer.js index 39699128a3..9d8ecc632c 100644 --- a/src/ol/renderer/canvas/layer.js +++ b/src/ol/renderer/canvas/layer.js @@ -19,6 +19,12 @@ ol.renderer.canvas.Layer = function(layer) { ol.renderer.Layer.call(this, layer); + /** + * @protected + * @type {number} + */ + this.renderedResolution; + /** * @private * @type {ol.Transform} @@ -62,51 +68,6 @@ ol.renderer.canvas.Layer.prototype.clip = function(context, frameState, extent) }; -/** - * @param {olx.FrameState} frameState Frame state. - * @param {ol.LayerState} layerState Layer state. - * @param {CanvasRenderingContext2D} context Context. - */ -ol.renderer.canvas.Layer.prototype.composeFrame = function(frameState, layerState, context) { - - this.preCompose(context, frameState); - - var image = this.getImage(); - if (image) { - - // 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 imageTransform = this.getImageTransform(); - // 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 = context.globalAlpha; - context.globalAlpha = layerState.opacity; - - // for performance reasons, context.setTransform is only used - // when the view is rotated. see http://jsperf.com/canvas-transform - var dx = imageTransform[4]; - var dy = imageTransform[5]; - var dw = image.width * imageTransform[0]; - var dh = image.height * imageTransform[3]; - 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) { - context.restore(); - } - } - - this.postCompose(context, frameState, layerState); -}; - - /** * @param {ol.render.Event.Type} type Event type. * @param {CanvasRenderingContext2D} context Context. @@ -134,6 +95,27 @@ ol.renderer.canvas.Layer.prototype.dispatchComposeEvent_ = function(type, contex }; +/** + * @param {ol.Coordinate} coordinate Coordinate. + * @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.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; + } +}; + + /** * @param {CanvasRenderingContext2D} context Context. * @param {olx.FrameState} frameState Frame state. @@ -171,20 +153,6 @@ ol.renderer.canvas.Layer.prototype.dispatchRenderEvent = function(context, frame }; -/** - * @abstract - * @return {HTMLCanvasElement|HTMLVideoElement|Image} Canvas. - */ -ol.renderer.canvas.Layer.prototype.getImage = function() {}; - - -/** - * @abstract - * @return {!ol.Transform} Image transform. - */ -ol.renderer.canvas.Layer.prototype.getImageTransform = function() {}; - - /** * @param {olx.FrameState} frameState Frame state. * @param {number} offsetX Offset on the x-axis in view coordinates. @@ -205,6 +173,14 @@ ol.renderer.canvas.Layer.prototype.getTransform = function(frameState, offsetX) }; +/** + * @abstract + * @param {olx.FrameState} frameState Frame state. + * @param {ol.LayerState} layerState Layer state. + * @param {CanvasRenderingContext2D} context Context. + */ +ol.renderer.canvas.Layer.prototype.composeFrame = function(frameState, layerState, context) {}; + /** * @abstract * @param {olx.FrameState} frameState Frame state. @@ -212,24 +188,3 @@ ol.renderer.canvas.Layer.prototype.getTransform = function(frameState, offsetX) * @return {boolean} whether composeFrame should be called. */ ol.renderer.canvas.Layer.prototype.prepareFrame = function(frameState, layerState) {}; - - -/** - * @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.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/tilelayer.js b/src/ol/renderer/canvas/tilelayer.js index 5c4ee410b9..fdf2ce91ca 100644 --- a/src/ol/renderer/canvas/tilelayer.js +++ b/src/ol/renderer/canvas/tilelayer.js @@ -9,17 +9,17 @@ goog.require('ol.Tile'); goog.require('ol.array'); goog.require('ol.dom'); goog.require('ol.extent'); -goog.require('ol.renderer.canvas.Layer'); +goog.require('ol.renderer.canvas.IntermediateCanvas'); /** * @constructor - * @extends {ol.renderer.canvas.Layer} + * @extends {ol.renderer.canvas.IntermediateCanvas} * @param {ol.layer.Tile|ol.layer.VectorTile} tileLayer Tile layer. */ ol.renderer.canvas.TileLayer = function(tileLayer) { - ol.renderer.canvas.Layer.call(this, tileLayer); + ol.renderer.canvas.IntermediateCanvas.call(this, tileLayer); /** * @protected @@ -33,12 +33,6 @@ ol.renderer.canvas.TileLayer = function(tileLayer) { */ this.renderedExtent_ = null; - /** - * @private - * @type {number} - */ - this.renderedResolution_; - /** * @private * @type {number} @@ -75,12 +69,6 @@ ol.renderer.canvas.TileLayer = function(tileLayer) { */ this.imageTransform_ = ol.transform.create(); - /** - * @private - * @type {ol.Transform} - */ - this.coordinateToCanvasPixelTransform_ = ol.transform.create(); - /** * @protected * @type {number} @@ -88,7 +76,7 @@ ol.renderer.canvas.TileLayer = function(tileLayer) { this.zDirection = 0; }; -ol.inherits(ol.renderer.canvas.TileLayer, ol.renderer.canvas.Layer); +ol.inherits(ol.renderer.canvas.TileLayer, ol.renderer.canvas.IntermediateCanvas); /** @@ -172,7 +160,7 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function(frameState, layer } var hints = frameState.viewHints; - if (!(this.renderedResolution_ && Date.now() - frameState.time > 16 && + 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)) || @@ -220,18 +208,18 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = function(frameState, layer } this.renderedRevision_ = sourceRevision; - this.renderedResolution_ = tileResolution; + this.renderedResolution = tileResolution; this.renderedExtent_ = imageExtent; } - var scale = pixelRatio / tilePixelRatio * this.renderedResolution_ / viewResolution; + 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_, + 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, @@ -267,29 +255,6 @@ ol.renderer.canvas.TileLayer.prototype.drawTileImage = function(tile, frameState }; -/** - * @param {ol.Coordinate} coordinate Coordinate. - * @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.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; - if (imageData[3] > 0) { - return callback.call(thisArg, this.getLayer(), imageData); - } else { - return undefined; - } - -}; - - /** * @inheritDoc */ diff --git a/test/spec/ol/renderer/canvas/layer.test.js b/test/spec/ol/renderer/canvas/intermediatecanvas.test.js similarity index 91% rename from test/spec/ol/renderer/canvas/layer.test.js rename to test/spec/ol/renderer/canvas/intermediatecanvas.test.js index 1eae42142e..ee6234ecb4 100644 --- a/test/spec/ol/renderer/canvas/layer.test.js +++ b/test/spec/ol/renderer/canvas/intermediatecanvas.test.js @@ -3,17 +3,17 @@ goog.provide('ol.test.renderer.canvas.Layer'); goog.require('ol.transform'); goog.require('ol.layer.Image'); goog.require('ol.renderer.Map'); -goog.require('ol.renderer.canvas.Layer'); +goog.require('ol.renderer.canvas.IntermediateCanvas'); -describe('ol.renderer.canvas.Layer', function() { +describe('ol.renderer.canvas.IntermediateCanvas', 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 renderer = new ol.renderer.canvas.IntermediateCanvas(layer); var image = new Image(); image.width = 3; image.height = 3;