diff --git a/externs/olx.js b/externs/olx.js index 467d2eb6ea..51f8a54c38 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -4501,6 +4501,8 @@ olx.source.ImageVectorOptions.prototype.style; /** * @typedef {{sources: Array., * operations: (Array.|undefined), + * lib: (Object|undefined), + * threads: (number|undefined), * operationType: (ol.raster.OperationType|undefined)}} * @api */ @@ -4524,6 +4526,26 @@ olx.source.RasterOptions.prototype.sources; olx.source.RasterOptions.prototype.operations; +/** + * Functions that will be made available to operations run in a worker. + * @type {Object|undefined} + * @api + */ +olx.source.RasterOptions.prototype.lib; + + +/** + * By default, operations will be run in a single worker thread. To avoid using + * workers altogether, set `threads: 0`. For pixel operations, operations can + * be run in multiple worker threads. Note that there some additional overhead + * in transferring data to multiple workers, and that depending on the user's + * system, it may not be possible to parallelize the work. + * @type {number|undefined} + * @api + */ +olx.source.RasterOptions.prototype.threads; + + /** * Operation type. Supported values are `'pixel'` and `'image'`. By default, * `'pixel'` operations are assumed, and operations will be called with an diff --git a/package.json b/package.json index 07f3e6da85..a0dafaeda5 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "metalsmith": "1.6.0", "metalsmith-templates": "0.7.0", "nomnom": "1.8.0", - "pixelworks": "^0.10.1", + "pixelworks": "^0.11.0", "rbush": "1.3.5", "temp": "0.8.1", "walk": "2.3.4", diff --git a/src/ol/source/rastersource.js b/src/ol/source/rastersource.js index 02ed2c6879..46b4d13502 100644 --- a/src/ol/source/rastersource.js +++ b/src/ol/source/rastersource.js @@ -3,7 +3,9 @@ goog.provide('ol.source.RasterEvent'); goog.provide('ol.source.RasterEventType'); goog.require('goog.asserts'); +goog.require('goog.events'); goog.require('goog.events.Event'); +goog.require('goog.events.EventType'); goog.require('goog.functions'); goog.require('goog.object'); goog.require('goog.vec.Mat4'); @@ -47,11 +49,17 @@ ol.source.Raster = function(options) { var operations = goog.isDef(options.operations) ? options.operations : [ol.raster.IdentityOp]; + /** + * @private + * @type {number} + */ + this.threads_ = goog.isDef(options.threads) ? options.threads : 1; + /** * @private * @type {*} */ - this.worker_ = this.createWorker_(operations); + this.worker_ = this.createWorker_(operations, options.lib, this.threads_); /** * @private @@ -59,6 +67,11 @@ ol.source.Raster = function(options) { */ this.renderers_ = ol.source.Raster.createRenderers_(options.sources); + for (var r = 0, rr = this.renderers_.length; r < rr; ++r) { + goog.events.listen(this.renderers_[r], goog.events.EventType.CHANGE, + this.changed, false, this); + } + /** * @private * @type {CanvasRenderingContext2D} @@ -134,14 +147,19 @@ goog.inherits(ol.source.Raster, ol.source.Image); /** * Create a worker. * @param {Array.} operations The operations. + * @param {Object=} opt_lib Optional lib functions. + * @param {number=} opt_threads Number of threads. * @return {*} The worker. * @private */ -ol.source.Raster.prototype.createWorker_ = function(operations) { +ol.source.Raster.prototype.createWorker_ = + function(operations, opt_lib, opt_threads) { return new ol.ext.pixelworks.Processor({ operations: operations, imageOps: this.operationType_ === ol.raster.OperationType.IMAGE, - queue: 1 + queue: 1, + lib: opt_lib, + threads: opt_threads }); }; @@ -149,10 +167,12 @@ ol.source.Raster.prototype.createWorker_ = function(operations) { /** * Reset the operations. * @param {Array.} operations New operations. + * @param {Object=} opt_lib Functions that will be available to operations run + * in a worker. * @api */ -ol.source.Raster.prototype.setOperations = function(operations) { - this.worker_ = this.createWorker_(operations); +ol.source.Raster.prototype.setOperations = function(operations, opt_lib) { + this.worker_ = this.createWorker_(operations, opt_lib, this.threads_); this.changed(); }; @@ -281,10 +301,15 @@ ol.source.Raster.prototype.composeFrame_ = function(frameState, callback) { var len = this.renderers_.length; var imageDatas = new Array(len); for (var i = 0; i < len; ++i) { - imageDatas[i] = ol.source.Raster.getImageData_( + var imageData = ol.source.Raster.getImageData_( this.renderers_[i], frameState, frameState.layerStatesArray[i]); + if (imageData) { + imageDatas[i] = imageData; + } else { + // image not yet ready + return; + } } - frameState.tileQueue.loadMoreTiles(16, 16); var data = {}; this.dispatchEvent(new ol.source.RasterEvent( @@ -292,6 +317,8 @@ ol.source.Raster.prototype.composeFrame_ = function(frameState, callback) { this.worker_.process(imageDatas, data, this.onWorkerComplete_.bind(this, frameState, callback)); + + frameState.tileQueue.loadMoreTiles(16, 16); }; @@ -337,16 +364,50 @@ ol.source.Raster.prototype.onWorkerComplete_ = */ ol.source.Raster.getImageData_ = function(renderer, frameState, layerState) { renderer.prepareFrame(frameState, layerState); - var canvas = renderer.getImage(); + // 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 = goog.vec.Mat4.getElement(imageTransform, 0, 3); - var dy = goog.vec.Mat4.getElement(imageTransform, 1, 3); - return canvas.getContext('2d').getImageData( - Math.round(-dx), Math.round(-dy), - frameState.size[0], frameState.size[1]); + 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_) { + 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); + } + } + 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); + } }; +/** + * A reusable canvas context. + * @type {CanvasRenderingContext2D} + * @private + */ +ol.source.Raster.context_ = null; + + /** * Get a list of layer states from a list of renderers. * @param {Array.} renderers Layer renderers. diff --git a/test/spec/ol/source/rastersource.test.js b/test/spec/ol/source/rastersource.test.js new file mode 100644 index 0000000000..69e8f71914 --- /dev/null +++ b/test/spec/ol/source/rastersource.test.js @@ -0,0 +1,307 @@ +goog.provide('ol.test.source.RasterSource'); + +var red = 'data:image/gif;base64,R0lGODlhAQABAPAAAP8AAP///yH5BAAAAAAALAAAAAA' + + 'BAAEAAAICRAEAOw=='; + +var green = 'data:image/gif;base64,R0lGODlhAQABAPAAAAD/AP///yH5BAAAAAAALAAAA' + + 'AABAAEAAAICRAEAOw=='; + +var blue = 'data:image/gif;base64,R0lGODlhAQABAPAAAAAA/////yH5BAAAAAAALAAAAA' + + 'ABAAEAAAICRAEAOw=='; + +function itNoPhantom() { + if (window.mochaPhantomJS) { + return xit.apply(this, arguments); + } else { + return it.apply(this, arguments); + } +} + +describe('ol.source.Raster', function() { + + var target, map, redSource, greenSource, blueSource; + + beforeEach(function() { + target = document.createElement('div'); + + var style = target.style; + style.position = 'absolute'; + style.left = '-1000px'; + style.top = '-1000px'; + style.width = '2px'; + style.height = '2px'; + document.body.appendChild(target); + + var extent = [-1, -1, 1, 1]; + + redSource = new ol.source.ImageStatic({ + url: red, + imageExtent: extent + }); + + greenSource = new ol.source.ImageStatic({ + url: green, + imageExtent: extent + }); + + blueSource = new ol.source.ImageStatic({ + url: blue, + imageExtent: extent + }); + + raster = new ol.source.Raster({ + threads: 0, + sources: [redSource, greenSource, blueSource], + operations: [function(inputs) { + return inputs; + }] + }); + + map = new ol.Map({ + target: target, + view: new ol.View({ + resolutions: [1], + projection: new ol.proj.Projection({ + code: 'image', + units: 'pixels', + extent: extent + }) + }), + layers: [ + new ol.layer.Image({ + source: raster + }) + ] + }); + }); + + afterEach(function() { + goog.dispose(map); + document.body.removeChild(target); + }); + + describe('constructor', function() { + + it('returns a tile source', function() { + var source = new ol.source.Raster({ + threads: 0, + sources: [new ol.source.Tile({})] + }); + expect(source).to.be.a(ol.source.Source); + expect(source).to.be.a(ol.source.Raster); + }); + + itNoPhantom('defaults to "pixel" operations', function(done) { + + var log = []; + + raster = new ol.source.Raster({ + threads: 0, + sources: [redSource, greenSource, blueSource], + operations: [function(inputs) { + log.push(inputs); + return inputs; + }] + }); + + raster.on('afteroperations', function() { + expect(log.length).to.equal(4); + var inputs = log[0]; + var pixel = inputs[0]; + expect(pixel).to.be.an('array'); + done(); + }); + + map.getLayers().item(0).setSource(raster); + var view = map.getView(); + view.setCenter([0, 0]); + view.setZoom(0); + + }); + + itNoPhantom('allows operation type to be set to "image"', function(done) { + + var log = []; + + raster = new ol.source.Raster({ + operationType: ol.raster.OperationType.IMAGE, + threads: 0, + sources: [redSource, greenSource, blueSource], + operations: [function(inputs) { + log.push(inputs); + return inputs; + }] + }); + + raster.on('afteroperations', function() { + expect(log.length).to.equal(1); + var inputs = log[0]; + expect(inputs[0]).to.be.an(ImageData); + done(); + }); + + map.getLayers().item(0).setSource(raster); + var view = map.getView(); + view.setCenter([0, 0]); + view.setZoom(0); + + }); + + }); + + describe('#setOperations()', function() { + + itNoPhantom('allows operations to be set', function(done) { + + var count = 0; + raster.setOperations([function(pixels) { + ++count; + var redPixel = pixels[0]; + var greenPixel = pixels[1]; + var bluePixel = pixels[2]; + expect(redPixel).to.eql([255, 0, 0, 255]); + expect(greenPixel).to.eql([0, 255, 0, 255]); + expect(bluePixel).to.eql([0, 0, 255, 255]); + return pixels; + }]); + + var view = map.getView(); + view.setCenter([0, 0]); + view.setZoom(0); + + raster.on('afteroperations', function(event) { + expect(count).to.equal(4); + done(); + }); + + }); + + itNoPhantom('updates and re-runs the operations', function(done) { + + var view = map.getView(); + view.setCenter([0, 0]); + view.setZoom(0); + + var count = 0; + raster.on('afteroperations', function(event) { + ++count; + if (count === 1) { + raster.setOperations([function(inputs) { + return inputs; + }]); + } else { + done(); + } + }); + + }); + + }); + + describe('beforeoperations', function() { + + itNoPhantom('gets called before operations are run', function(done) { + + var count = 0; + raster.setOperations([function(inputs) { + ++count; + return inputs; + }]); + + raster.on('beforeoperations', function(event) { + expect(count).to.equal(0); + expect(!!event).to.be(true); + expect(event.extent).to.be.an('array'); + expect(event.resolution).to.be.a('number'); + expect(event.data).to.be.an('object'); + done(); + }); + + var view = map.getView(); + view.setCenter([0, 0]); + view.setZoom(0); + + }); + + + itNoPhantom('allows data to be set for the operations', function(done) { + + raster.setOperations([function(inputs, data) { + ++data.count; + return inputs; + }]); + + raster.on('beforeoperations', function(event) { + event.data.count = 0; + }); + + raster.on('afteroperations', function(event) { + expect(event.data.count).to.equal(4); + done(); + }); + + var view = map.getView(); + view.setCenter([0, 0]); + view.setZoom(0); + + }); + + }); + + describe('afteroperations', function() { + + itNoPhantom('gets called after operations are run', function(done) { + + var count = 0; + raster.setOperations([function(inputs) { + ++count; + return inputs; + }]); + + raster.on('afteroperations', function(event) { + expect(count).to.equal(4); + expect(!!event).to.be(true); + expect(event.extent).to.be.an('array'); + expect(event.resolution).to.be.a('number'); + expect(event.data).to.be.an('object'); + done(); + }); + + var view = map.getView(); + view.setCenter([0, 0]); + view.setZoom(0); + + }); + + itNoPhantom('receives data set by the operations', function(done) { + + raster.setOperations([function(inputs, data) { + data.message = 'hello world'; + return inputs; + }]); + + raster.on('afteroperations', function(event) { + expect(event.data.message).to.equal('hello world'); + done(); + }); + + var view = map.getView(); + view.setCenter([0, 0]); + view.setZoom(0); + + }); + + }); + +}); + +goog.require('ol.Map'); +goog.require('ol.View'); +goog.require('ol.layer.Image'); +goog.require('ol.proj.Projection'); +goog.require('ol.raster.OperationType'); +goog.require('ol.source.Image'); +goog.require('ol.source.ImageStatic'); +goog.require('ol.source.Raster'); +goog.require('ol.source.Source'); +goog.require('ol.source.Tile');