diff --git a/examples/raster.js b/examples/raster.js index feeb037888..d86e9d83c3 100644 --- a/examples/raster.js +++ b/examples/raster.js @@ -5,7 +5,7 @@ import XYZ from '../src/ol/source/XYZ.js'; import {Image as ImageLayer, Tile as TileLayer} from '../src/ol/layer.js'; const minVgi = 0; -const maxVgi = 0.25; +const maxVgi = 0.5; const bins = 10; /** @@ -87,7 +87,7 @@ const raster = new RasterSource({ summarize: summarize, }, }); -raster.set('threshold', 0.1); +raster.set('threshold', 0.25); function createCounts(min, max, num) { const values = new Array(num); diff --git a/src/ol/source/Raster.js b/src/ol/source/Raster.js index 22898169ca..3528ea3b51 100644 --- a/src/ol/source/Raster.js +++ b/src/ol/source/Raster.js @@ -46,12 +46,21 @@ export function newImageData(data, width, height) { return imageData; } +/** + * @typedef {Object} MinionData + * @property {Array} buffers Array of buffers. + * @property {Object} meta Operation metadata. + * @property {boolean} imageOps The operation is an image operation. + * @property {number} width The width of the image. + * @property {number} height The height of the image. + */ + /* istanbul ignore next */ /** * Create a function for running operations. This function is serialized for * use in a worker. * @param {function(Array, Object):*} operation The operation. - * @return {function(Object):ArrayBuffer} A function that takes an object with + * @return {function(MinionData):ArrayBuffer} A function that takes an object with * buffers, meta, imageOps, width, and height properties and returns an array * buffer. */ @@ -81,40 +90,40 @@ function createMinion(operation) { const numBuffers = buffers.length; const numBytes = buffers[0].byteLength; - let output, b; if (imageOps) { const images = new Array(numBuffers); - for (b = 0; b < numBuffers; ++b) { + for (let b = 0; b < numBuffers; ++b) { images[b] = newWorkerImageData( new Uint8ClampedArray(buffers[b]), width, height ); } - output = operation(images, meta).data; - } else { - output = new Uint8ClampedArray(numBytes); - const arrays = new Array(numBuffers); - const pixels = new Array(numBuffers); - for (b = 0; b < numBuffers; ++b) { - arrays[b] = new Uint8ClampedArray(buffers[b]); - pixels[b] = [0, 0, 0, 0]; - } - for (let i = 0; i < numBytes; i += 4) { - for (let j = 0; j < numBuffers; ++j) { - const array = arrays[j]; - pixels[j][0] = array[i]; - pixels[j][1] = array[i + 1]; - pixels[j][2] = array[i + 2]; - pixels[j][3] = array[i + 3]; - } - const pixel = operation(pixels, meta); - output[i] = pixel[0]; - output[i + 1] = pixel[1]; - output[i + 2] = pixel[2]; - output[i + 3] = pixel[3]; + const output = operation(images, meta).data; + return output.buffer; + } + + const output = new Uint8ClampedArray(numBytes); + const arrays = new Array(numBuffers); + const pixels = new Array(numBuffers); + for (let b = 0; b < numBuffers; ++b) { + arrays[b] = new Uint8ClampedArray(buffers[b]); + pixels[b] = [0, 0, 0, 0]; + } + for (let i = 0; i < numBytes; i += 4) { + for (let j = 0; j < numBuffers; ++j) { + const array = arrays[j]; + pixels[j][0] = array[i]; + pixels[j][1] = array[i + 1]; + pixels[j][2] = array[i + 2]; + pixels[j][3] = array[i + 3]; } + const pixel = operation(pixels, meta); + output[i] = pixel[0]; + output[i + 1] = pixel[1]; + output[i + 2] = pixel[2]; + output[i + 3] = pixel[3]; } return output.buffer; }; @@ -122,7 +131,7 @@ function createMinion(operation) { /** * Create a worker for running operations. - * @param {Object} config Configuration. + * @param {ProcessorOptions} config Processor options. * @param {function(MessageEvent): void} onMessage Called with a message event. * @return {Worker} The worker. */ @@ -177,11 +186,22 @@ function createFauxWorker(config, onMessage) { }; } +/** + * @typedef {function(Error, ImageData, (Object|Array)): void} JobCallback + */ + +/** + * @typedef {Object} Job + * @property {Object} meta Job metadata. + * @property {Array} inputs Array of input data. + * @property {JobCallback} callback Called when the job is complete. + */ + /** * @typedef {Object} ProcessorOptions * @property {number} threads Number of workers to spawn. - * @property {function(Array, Object):*} operation The operation. - * @property {Object} [lib] Functions that will be made available to operations run in a worker. + * @property {Operation} operation The operation. + * @property {Object} [lib] Functions that will be made available to operations run in a worker. * @property {number} queue The number of queued jobs to allow. * @property {boolean} [imageOps=false] Pass all the image data to the operation instead of a single pixel. */ @@ -206,7 +226,11 @@ export class Processor extends Disposable { } else { threads = config.threads || 1; } - const workers = []; + + /** + * @type {Array} + */ + const workers = new Array(threads); if (threads) { for (let i = 0; i < threads; ++i) { workers[i] = createWorker(config, this._onWorkerMessage.bind(this, i)); @@ -218,17 +242,32 @@ export class Processor extends Disposable { ); } this._workers = workers; + + /** + * @type {Array} + * @private + */ this._queue = []; + this._maxQueueLength = config.queue || Infinity; this._running = 0; + + /** + * @type {Object} + * @private + */ this._dataLookup = {}; + + /** + * @type {Job} + * @private + */ this._job = null; } /** * Run operation on input data. - * @param {Array} inputs Array of pixels or image data - * (depending on the operation type). + * @param {Array} inputs Array of image data. * @param {Object} meta A user data object. This is passed to all operations * and must be serializable. * @param {function(Error, ImageData, Object): void} callback Called when work @@ -246,7 +285,7 @@ export class Processor extends Disposable { /** * Add a job to the queue. - * @param {Object} job The job. + * @param {Job} job The job. */ _enqueue(job) { this._queue.push(job); @@ -259,48 +298,51 @@ export class Processor extends Disposable { * Dispatch a job. */ _dispatch() { - if (this._running === 0 && this._queue.length > 0) { - const job = this._queue.shift(); - this._job = job; - const width = job.inputs[0].width; - const height = job.inputs[0].height; - const buffers = job.inputs.map(function (input) { - return input.data.buffer; - }); - const threads = this._workers.length; - this._running = threads; - if (threads === 1) { - this._workers[0].postMessage( - { - buffers: buffers, - meta: job.meta, - imageOps: this._imageOps, - width: width, - height: height, - }, - buffers - ); - } else { - const length = job.inputs[0].data.length; - const segmentLength = 4 * Math.ceil(length / 4 / threads); - for (let i = 0; i < threads; ++i) { - const offset = i * segmentLength; - const slices = []; - for (let j = 0, jj = buffers.length; j < jj; ++j) { - slices.push(buffers[j].slice(offset, offset + segmentLength)); - } - this._workers[i].postMessage( - { - buffers: slices, - meta: job.meta, - imageOps: this._imageOps, - width: width, - height: height, - }, - slices - ); - } + if (this._running || this._queue.length === 0) { + return; + } + + const job = this._queue.shift(); + this._job = job; + const width = job.inputs[0].width; + const height = job.inputs[0].height; + const buffers = job.inputs.map(function (input) { + return input.data.buffer; + }); + const threads = this._workers.length; + this._running = threads; + if (threads === 1) { + this._workers[0].postMessage( + { + buffers: buffers, + meta: job.meta, + imageOps: this._imageOps, + width: width, + height: height, + }, + buffers + ); + return; + } + + const length = job.inputs[0].data.length; + const segmentLength = 4 * Math.ceil(length / 4 / threads); + for (let i = 0; i < threads; ++i) { + const offset = i * segmentLength; + const slices = []; + for (let j = 0, jj = buffers.length; j < jj; ++j) { + slices.push(buffers[j].slice(offset, offset + segmentLength)); } + this._workers[i].postMessage( + { + buffers: slices, + meta: job.meta, + imageOps: this._imageOps, + width: width, + height: height, + }, + slices + ); } } @@ -334,7 +376,7 @@ export class Processor extends Disposable { } else { const length = job.inputs[0].data.length; data = new Uint8ClampedArray(length); - meta = new Array(length); + meta = new Array(threads); const segmentLength = 4 * Math.ceil(length / 4 / threads); for (let i = 0; i < threads; ++i) { const buffer = this._dataLookup[i]['buffer']; @@ -388,14 +430,17 @@ export class Processor extends Disposable { */ const RasterEventType = { /** - * Triggered before operations are run. + * Triggered before operations are run. Listeners will receive an event object with + * a `data` property that can be used to make data available to operations. * @event module:ol/source/Raster.RasterSourceEvent#beforeoperations * @api */ BEFOREOPERATIONS: 'beforeoperations', /** - * Triggered after operations are run. + * Triggered after operations are run. Listeners will receive an event object with + * a `data` property. If more than one thread is used, `data` will be an array of + * objects. If a single thread is used, `data` will be a single object. * @event module:ol/source/Raster.RasterSourceEvent#afteroperations * @api */ @@ -424,7 +469,8 @@ export class RasterSourceEvent extends Event { /** * @param {string} type Type. * @param {import("../PluggableMap.js").FrameState} frameState The frame state. - * @param {Object} data An object made available to operations. + * @param {Object|Array} data An object made available to operations. For "afteroperations" evenets + * this will be an array of objects if more than one thread is used. */ constructor(type, frameState, data) { super(type); @@ -776,7 +822,7 @@ class RasterSource extends ImageSource { * @param {import("../PluggableMap.js").FrameState} frameState The frame state. * @param {Error} err Any error during processing. * @param {ImageData} output The output image data. - * @param {Object} data The user data. + * @param {Object|Array} data The user data (or an array if more than one thread). * @private */ onWorkerComplete_(frameState, err, output, data) { diff --git a/test/browser/spec/ol/source/raster.test.js b/test/browser/spec/ol/source/raster.test.js index cb871ab677..eb24be69dc 100644 --- a/test/browser/spec/ol/source/raster.test.js +++ b/test/browser/spec/ol/source/raster.test.js @@ -26,7 +26,7 @@ const green = 'AABAAEAAAICRAEAOw=='; where('Uint8ClampedArray').describe('ol.source.Raster', function () { - let map, target, redSource, greenSource, blueSource, raster; + let map, target, redSource, greenSource, blueSource, layer, raster; beforeEach(function () { target = document.createElement('div'); @@ -73,6 +73,10 @@ where('Uint8ClampedArray').describe('ol.source.Raster', function () { }, }); + layer = new ImageLayer({ + source: raster, + }); + map = new Map({ target: target, view: new View({ @@ -83,11 +87,7 @@ where('Uint8ClampedArray').describe('ol.source.Raster', function () { extent: extent, }), }), - layers: [ - new ImageLayer({ - source: raster, - }), - ], + layers: [layer], }); }); @@ -366,6 +366,32 @@ where('Uint8ClampedArray').describe('ol.source.Raster', function () { view.setCenter([0, 0]); view.setZoom(0); }); + + it('is passed an array of data if more than one thread', function (done) { + const threads = 3; + + raster = new RasterSource({ + threads: threads, + sources: [redSource, greenSource, blueSource], + operation: function (inputs, data) { + data.prop = 'value'; + return inputs[0]; + }, + }); + + layer.setSource(raster); + + raster.once('afteroperations', function (event) { + expect(event.data).to.be.an(Array); + expect(event.data).to.have.length(threads); + expect(event.data[0].prop).to.equal('value'); + done(); + }); + + const view = map.getView(); + view.setCenter([0, 0]); + view.setZoom(0); + }); }); describe('tile loading', function () {