diff --git a/package.json b/package.json index cf4de3382e..4c7ecc38c0 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "async": "0.9.0", + "browserify": "9.0.3", "closure-util": "1.5.0", "fs-extra": "0.12.0", "glob": "5.0.3", @@ -37,11 +38,11 @@ "metalsmith": "1.6.0", "metalsmith-templates": "0.7.0", "nomnom": "1.8.0", + "pixelworks": "^0.8.0", "rbush": "1.3.5", "temp": "0.8.1", "walk": "2.3.4", - "wrench": "1.5.8", - "browserify": "9.0.3" + "wrench": "1.5.8" }, "devDependencies": { "clean-css": "2.2.16", @@ -61,6 +62,7 @@ "slimerjs-edge": "0.10.0-pre-2" }, "ext": [ - "rbush" + "rbush", + {"module": "pixelworks", "browserify": true} ] } diff --git a/src/ol/imagecanvas.js b/src/ol/imagecanvas.js index 6cc9b7295b..3d903bab06 100644 --- a/src/ol/imagecanvas.js +++ b/src/ol/imagecanvas.js @@ -1,5 +1,6 @@ goog.provide('ol.ImageCanvas'); +goog.require('goog.asserts'); goog.require('ol.ImageBase'); goog.require('ol.ImageState'); @@ -13,12 +14,23 @@ goog.require('ol.ImageState'); * @param {number} pixelRatio Pixel ratio. * @param {Array.} attributions Attributions. * @param {HTMLCanvasElement} canvas Canvas. + * @param {ol.ImageCanvasLoader=} opt_loader Optional loader function to + * support asynchronous canvas drawing. */ ol.ImageCanvas = function(extent, resolution, pixelRatio, attributions, - canvas) { + canvas, opt_loader) { - goog.base(this, extent, resolution, pixelRatio, ol.ImageState.LOADED, - attributions); + /** + * Optional canvas loader function. + * @type {?ol.ImageCanvasLoader} + * @private + */ + this.loader_ = goog.isDef(opt_loader) ? opt_loader : null; + + var state = goog.isDef(opt_loader) ? + ol.ImageState.IDLE : ol.ImageState.LOADED; + + goog.base(this, extent, resolution, pixelRatio, state, attributions); /** * @private @@ -26,13 +38,68 @@ ol.ImageCanvas = function(extent, resolution, pixelRatio, attributions, */ this.canvas_ = canvas; + /** + * @private + * @type {Error} + */ + this.error_ = null; + }; goog.inherits(ol.ImageCanvas, ol.ImageBase); +/** + * Get any error associated with asynchronous rendering. + * @return {Error} Any error that occurred during rendering. + */ +ol.ImageCanvas.prototype.getError = function() { + return this.error_; +}; + + +/** + * Handle async drawing complete. + * @param {Error} err Any error during drawing. + * @private + */ +ol.ImageCanvas.prototype.handleLoad_ = function(err) { + if (err) { + this.error_ = err; + this.state = ol.ImageState.ERROR; + } else { + this.state = ol.ImageState.LOADED; + } + this.changed(); +}; + + +/** + * Trigger drawing on canvas. + */ +ol.ImageCanvas.prototype.load = function() { + if (this.state == ol.ImageState.IDLE) { + goog.asserts.assert(!goog.isNull(this.loader_)); + this.state = ol.ImageState.LOADING; + this.changed(); + this.loader_(goog.bind(this.handleLoad_, this)); + } +}; + + /** * @inheritDoc */ ol.ImageCanvas.prototype.getImage = function(opt_context) { return this.canvas_; }; + + +/** + * A function that is called to trigger asynchronous canvas drawing. It is + * called with a "done" callback that should be called when drawing is done. + * If any error occurs during drawing, the "done" callback should be called with + * that error. + * + * @typedef {function(function(Error))} + */ +ol.ImageCanvasLoader; diff --git a/src/ol/source/rastersource.js b/src/ol/source/rastersource.js index 09bc5f28f3..8a8f0cb39f 100644 --- a/src/ol/source/rastersource.js +++ b/src/ol/source/rastersource.js @@ -5,10 +5,12 @@ goog.provide('ol.source.RasterEventType'); goog.require('goog.asserts'); goog.require('goog.events.Event'); goog.require('goog.functions'); +goog.require('goog.object'); goog.require('goog.vec.Mat4'); goog.require('ol.ImageCanvas'); goog.require('ol.TileQueue'); goog.require('ol.dom'); +goog.require('ol.ext.pixelworks'); goog.require('ol.extent'); goog.require('ol.layer.Image'); goog.require('ol.layer.Tile'); @@ -35,19 +37,14 @@ goog.require('ol.source.Tile'); */ ol.source.Raster = function(options) { - /** - * @private - * @type {Array.} - */ - this.operations_ = goog.isDef(options.operations) ? + var operations = goog.isDef(options.operations) ? options.operations : [ol.raster.IdentityOp]; - /** - * @private - * @type {ol.raster.OperationType} - */ - this.operationType_ = goog.isDef(options.operationType) ? - options.operationType : ol.raster.OperationType.PIXEL; + this.worker_ = new ol.ext.pixelworks.Processor({ + operations: operations, + imageOps: options.operationType === ol.raster.OperationType.IMAGE, + queue: 1 + }); /** * @private @@ -75,6 +72,20 @@ ol.source.Raster = function(options) { layerStates[goog.getUid(layerStatesArray[i].layer)] = layerStatesArray[i]; } + /** + * The most recently rendered state. + * @type {?ol.source.Raster.RenderedState} + * @private + */ + this.renderedState_ = null; + + /** + * The most recently rendered image canvas. + * @type {ol.ImageCanvas} + * @private + */ + this.renderedImageCanvas_ = null; + /** * @private * @type {olx.FrameState} @@ -119,7 +130,7 @@ goog.inherits(ol.source.Raster, ol.source.Image); * @api */ ol.source.Raster.prototype.setOperations = function(operations) { - this.operations_ = operations; + this.worker_.setOperations(operations); this.changed(); }; @@ -134,7 +145,12 @@ ol.source.Raster.prototype.setOperations = function(operations) { */ ol.source.Raster.prototype.updateFrameState_ = function(extent, resolution, projection) { - var frameState = this.frameState_; + + var frameState = /** @type {olx.FrameState} */ ( + goog.object.clone(this.frameState_)); + + frameState.viewState = /** @type {olx.ViewState} */ ( + goog.object.clone(frameState.viewState)); var center = ol.extent.getCenter(extent); var width = Math.round(ol.extent.getWidth(extent) / resolution); @@ -153,12 +169,36 @@ ol.source.Raster.prototype.updateFrameState_ = }; +/** + * Determine if the most recently rendered image canvas is dirty. + * @param {ol.Extent} extent The requested extent. + * @param {number} resolution The requested resolution. + * @return {boolean} The image is dirty. + * @private + */ +ol.source.Raster.prototype.isDirty_ = function(extent, resolution) { + var state = this.renderedState_; + return !state || + this.getRevision() !== state.revision || + resolution !== state.resolution || + !ol.extent.equals(extent, state.extent); +}; + + /** * @inheritDoc */ ol.source.Raster.prototype.getImage = function(extent, resolution, pixelRatio, projection) { + if (!this.allSourcesReady_()) { + return null; + } + + if (!this.isDirty_(extent, resolution)) { + return this.renderedImageCanvas_; + } + var context = this.canvasContext_; var canvas = context.canvas; @@ -172,10 +212,18 @@ ol.source.Raster.prototype.getImage = } var frameState = this.updateFrameState_(extent, resolution, projection); - this.composeFrame_(frameState); - var imageCanvas = new ol.ImageCanvas(extent, resolution, 1, - this.getAttributions(), canvas); + var imageCanvas = new ol.ImageCanvas( + extent, resolution, 1, this.getAttributions(), canvas, + this.composeFrame_.bind(this, frameState)); + + this.renderedImageCanvas_ = imageCanvas; + + this.renderedState_ = { + extent: extent, + resolution: resolution, + revision: this.getRevision() + }; return imageCanvas; }; @@ -204,93 +252,53 @@ ol.source.Raster.prototype.allSourcesReady_ = function() { * Compose the frame. This renders data from all sources, runs pixel-wise * operations, and renders the result to the stored canvas context. * @param {olx.FrameState} frameState The frame state. + * @param {function(Error)} callback Called when composition is complete. * @private */ -ol.source.Raster.prototype.composeFrame_ = function(frameState) { - if (!this.allSourcesReady_()) { - return; - } +ol.source.Raster.prototype.composeFrame_ = function(frameState, callback) { var len = this.renderers_.length; var imageDatas = new Array(len); - var pixels = new Array(len); - - var context = this.canvasContext_; - var canvas = context.canvas; - for (var i = 0; i < len; ++i) { - pixels[i] = [0, 0, 0, 0]; imageDatas[i] = ol.source.Raster.getImageData_( this.renderers_[i], frameState, frameState.layerStatesArray[i]); } + frameState.tileQueue.loadMoreTiles(16, 16); var data = {}; this.dispatchEvent(new ol.source.RasterEvent( ol.source.RasterEventType.BEFOREOPERATIONS, frameState, data)); - var targetImageData = null; - if (this.operationType_ === ol.raster.OperationType.PIXEL) { - targetImageData = context.getImageData(0, 0, canvas.width, - canvas.height); - var target = targetImageData.data; + this.worker_.process(imageDatas, data, + this.onWorkerComplete_.bind(this, frameState, data, callback)); +}; - var source, pixel; - for (var j = 0, jj = target.length; j < jj; j += 4) { - for (var k = 0; k < len; ++k) { - source = imageDatas[k].data; - pixel = pixels[k]; - pixel[0] = source[j]; - pixel[1] = source[j + 1]; - pixel[2] = source[j + 2]; - pixel[3] = source[j + 3]; - } - pixel = this.runPixelOperations_(pixels, data)[0]; - target[j] = pixel[0]; - target[j + 1] = pixel[1]; - target[j + 2] = pixel[2]; - target[j + 3] = pixel[3]; - } - } else if (this.operationType_ === ol.raster.OperationType.IMAGE) { - targetImageData = this.runImageOperations_(imageDatas, data)[0]; - } else { - goog.asserts.fail('unsupported operation type: ' + this.operationType_); + +/** + * Called when pixel processing is complete. + * @param {olx.FrameState} frameState The frame state. + * @param {Object} data The user data. + * @param {function(Error)} callback Called when rendering is complete. + * @param {Error} err Any error during processing. + * @param {ImageData} output The output image data. + * @private + */ +ol.source.Raster.prototype.onWorkerComplete_ = + function(frameState, data, callback, err, output) { + if (err) { + callback(err); + return; + } + if (goog.isNull(output)) { + // job aborted + return; } this.dispatchEvent(new ol.source.RasterEvent( ol.source.RasterEventType.AFTEROPERATIONS, frameState, data)); - context.putImageData(targetImageData, 0, 0); + this.canvasContext_.putImageData(output, 0, 0); - frameState.tileQueue.loadMoreTiles(16, 16); -}; - - -/** - * Run pixel-wise operations to transform pixels. - * @param {Array.} pixels The input pixels. - * @param {Object} data User storage. - * @return {Array.} The modified pixels. - * @private - */ -ol.source.Raster.prototype.runPixelOperations_ = function(pixels, data) { - for (var i = 0, ii = this.operations_.length; i < ii; ++i) { - pixels = this.operations_[i](pixels, data); - } - return pixels; -}; - - -/** - * Run image operations. - * @param {Array.} imageDatas The input image data. - * @param {Object} data User storage. - * @return {Array.} The output image data. - * @private - */ -ol.source.Raster.prototype.runImageOperations_ = function(imageDatas, data) { - for (var i = 0, ii = this.operations_.length; i < ii; ++i) { - imageDatas = this.operations_[i](imageDatas, data); - } - return imageDatas; + callback(null); }; @@ -388,6 +396,14 @@ ol.source.Raster.createTileRenderer_ = function(source) { }; +/** + * @typedef {{revision: number, + * resolution: number, + * extent: ol.Extent}} + */ +ol.source.Raster.RenderedState; + + /** * @classdesc