Allow UI thread to be used

Where workers are not available, or if operations are trivial to run, the main UI thread can be used instead.  This also adds tests that run in real browsers.
This commit is contained in:
Tim Schaub
2015-07-21 17:10:44 -06:00
parent af3c38052e
commit 0c486c522a
4 changed files with 404 additions and 14 deletions

View File

@@ -4501,6 +4501,8 @@ olx.source.ImageVectorOptions.prototype.style;
/** /**
* @typedef {{sources: Array.<ol.source.Source>, * @typedef {{sources: Array.<ol.source.Source>,
* operations: (Array.<ol.raster.Operation>|undefined), * operations: (Array.<ol.raster.Operation>|undefined),
* lib: (Object|undefined),
* threads: (number|undefined),
* operationType: (ol.raster.OperationType|undefined)}} * operationType: (ol.raster.OperationType|undefined)}}
* @api * @api
*/ */
@@ -4524,6 +4526,26 @@ olx.source.RasterOptions.prototype.sources;
olx.source.RasterOptions.prototype.operations; 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, * Operation type. Supported values are `'pixel'` and `'image'`. By default,
* `'pixel'` operations are assumed, and operations will be called with an * `'pixel'` operations are assumed, and operations will be called with an

View File

@@ -38,7 +38,7 @@
"metalsmith": "1.6.0", "metalsmith": "1.6.0",
"metalsmith-templates": "0.7.0", "metalsmith-templates": "0.7.0",
"nomnom": "1.8.0", "nomnom": "1.8.0",
"pixelworks": "^0.10.1", "pixelworks": "^0.11.0",
"rbush": "1.3.5", "rbush": "1.3.5",
"temp": "0.8.1", "temp": "0.8.1",
"walk": "2.3.4", "walk": "2.3.4",

View File

@@ -3,7 +3,9 @@ goog.provide('ol.source.RasterEvent');
goog.provide('ol.source.RasterEventType'); goog.provide('ol.source.RasterEventType');
goog.require('goog.asserts'); goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.Event'); goog.require('goog.events.Event');
goog.require('goog.events.EventType');
goog.require('goog.functions'); goog.require('goog.functions');
goog.require('goog.object'); goog.require('goog.object');
goog.require('goog.vec.Mat4'); goog.require('goog.vec.Mat4');
@@ -47,11 +49,17 @@ ol.source.Raster = function(options) {
var operations = goog.isDef(options.operations) ? var operations = goog.isDef(options.operations) ?
options.operations : [ol.raster.IdentityOp]; options.operations : [ol.raster.IdentityOp];
/**
* @private
* @type {number}
*/
this.threads_ = goog.isDef(options.threads) ? options.threads : 1;
/** /**
* @private * @private
* @type {*} * @type {*}
*/ */
this.worker_ = this.createWorker_(operations); this.worker_ = this.createWorker_(operations, options.lib, this.threads_);
/** /**
* @private * @private
@@ -59,6 +67,11 @@ ol.source.Raster = function(options) {
*/ */
this.renderers_ = ol.source.Raster.createRenderers_(options.sources); 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 * @private
* @type {CanvasRenderingContext2D} * @type {CanvasRenderingContext2D}
@@ -134,14 +147,19 @@ goog.inherits(ol.source.Raster, ol.source.Image);
/** /**
* Create a worker. * Create a worker.
* @param {Array.<ol.raster.Operation>} operations The operations. * @param {Array.<ol.raster.Operation>} operations The operations.
* @param {Object=} opt_lib Optional lib functions.
* @param {number=} opt_threads Number of threads.
* @return {*} The worker. * @return {*} The worker.
* @private * @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({ return new ol.ext.pixelworks.Processor({
operations: operations, operations: operations,
imageOps: this.operationType_ === ol.raster.OperationType.IMAGE, 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. * Reset the operations.
* @param {Array.<ol.raster.Operation>} operations New operations. * @param {Array.<ol.raster.Operation>} operations New operations.
* @param {Object=} opt_lib Functions that will be available to operations run
* in a worker.
* @api * @api
*/ */
ol.source.Raster.prototype.setOperations = function(operations) { ol.source.Raster.prototype.setOperations = function(operations, opt_lib) {
this.worker_ = this.createWorker_(operations); this.worker_ = this.createWorker_(operations, opt_lib, this.threads_);
this.changed(); this.changed();
}; };
@@ -281,10 +301,15 @@ ol.source.Raster.prototype.composeFrame_ = function(frameState, callback) {
var len = this.renderers_.length; var len = this.renderers_.length;
var imageDatas = new Array(len); var imageDatas = new Array(len);
for (var i = 0; i < len; ++i) { 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]); 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 = {}; var data = {};
this.dispatchEvent(new ol.source.RasterEvent( this.dispatchEvent(new ol.source.RasterEvent(
@@ -292,6 +317,8 @@ ol.source.Raster.prototype.composeFrame_ = function(frameState, callback) {
this.worker_.process(imageDatas, data, this.worker_.process(imageDatas, data,
this.onWorkerComplete_.bind(this, frameState, callback)); 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) { ol.source.Raster.getImageData_ = function(renderer, frameState, layerState) {
renderer.prepareFrame(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 imageTransform = renderer.getImageTransform();
var dx = goog.vec.Mat4.getElement(imageTransform, 0, 3); var dx = Math.round(goog.vec.Mat4.getElement(imageTransform, 0, 3));
var dy = goog.vec.Mat4.getElement(imageTransform, 1, 3); var dy = Math.round(goog.vec.Mat4.getElement(imageTransform, 1, 3));
return canvas.getContext('2d').getImageData( var width = frameState.size[0];
Math.round(-dx), Math.round(-dy), var height = frameState.size[1];
frameState.size[0], 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. * Get a list of layer states from a list of renderers.
* @param {Array.<ol.renderer.canvas.Layer>} renderers Layer renderers. * @param {Array.<ol.renderer.canvas.Layer>} renderers Layer renderers.

View File

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