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');