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:
@@ -4501,6 +4501,8 @@ olx.source.ImageVectorOptions.prototype.style;
|
||||
/**
|
||||
* @typedef {{sources: Array.<ol.source.Source>,
|
||||
* operations: (Array.<ol.raster.Operation>|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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.<ol.raster.Operation>} 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.<ol.raster.Operation>} 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.<ol.renderer.canvas.Layer>} renderers Layer renderers.
|
||||
|
||||
307
test/spec/ol/source/rastersource.test.js
Normal file
307
test/spec/ol/source/rastersource.test.js
Normal 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');
|
||||
Reference in New Issue
Block a user