Additional docs and type checking for raster source

This commit is contained in:
Tim Schaub
2021-08-26 11:04:07 -06:00
parent 3b6bf14cdc
commit de9ff20f65
3 changed files with 158 additions and 86 deletions

View File

@@ -5,7 +5,7 @@ import XYZ from '../src/ol/source/XYZ.js';
import {Image as ImageLayer, Tile as TileLayer} from '../src/ol/layer.js'; import {Image as ImageLayer, Tile as TileLayer} from '../src/ol/layer.js';
const minVgi = 0; const minVgi = 0;
const maxVgi = 0.25; const maxVgi = 0.5;
const bins = 10; const bins = 10;
/** /**
@@ -87,7 +87,7 @@ const raster = new RasterSource({
summarize: summarize, summarize: summarize,
}, },
}); });
raster.set('threshold', 0.1); raster.set('threshold', 0.25);
function createCounts(min, max, num) { function createCounts(min, max, num) {
const values = new Array(num); const values = new Array(num);

View File

@@ -46,12 +46,21 @@ export function newImageData(data, width, height) {
return imageData; return imageData;
} }
/**
* @typedef {Object} MinionData
* @property {Array<ArrayBuffer>} 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 */ /* istanbul ignore next */
/** /**
* Create a function for running operations. This function is serialized for * Create a function for running operations. This function is serialized for
* use in a worker. * use in a worker.
* @param {function(Array, Object):*} operation The operation. * @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 * buffers, meta, imageOps, width, and height properties and returns an array
* buffer. * buffer.
*/ */
@@ -81,23 +90,24 @@ function createMinion(operation) {
const numBuffers = buffers.length; const numBuffers = buffers.length;
const numBytes = buffers[0].byteLength; const numBytes = buffers[0].byteLength;
let output, b;
if (imageOps) { if (imageOps) {
const images = new Array(numBuffers); const images = new Array(numBuffers);
for (b = 0; b < numBuffers; ++b) { for (let b = 0; b < numBuffers; ++b) {
images[b] = newWorkerImageData( images[b] = newWorkerImageData(
new Uint8ClampedArray(buffers[b]), new Uint8ClampedArray(buffers[b]),
width, width,
height height
); );
} }
output = operation(images, meta).data; const output = operation(images, meta).data;
} else { return output.buffer;
output = new Uint8ClampedArray(numBytes); }
const output = new Uint8ClampedArray(numBytes);
const arrays = new Array(numBuffers); const arrays = new Array(numBuffers);
const pixels = new Array(numBuffers); const pixels = new Array(numBuffers);
for (b = 0; b < numBuffers; ++b) { for (let b = 0; b < numBuffers; ++b) {
arrays[b] = new Uint8ClampedArray(buffers[b]); arrays[b] = new Uint8ClampedArray(buffers[b]);
pixels[b] = [0, 0, 0, 0]; pixels[b] = [0, 0, 0, 0];
} }
@@ -115,14 +125,13 @@ function createMinion(operation) {
output[i + 2] = pixel[2]; output[i + 2] = pixel[2];
output[i + 3] = pixel[3]; output[i + 3] = pixel[3];
} }
}
return output.buffer; return output.buffer;
}; };
} }
/** /**
* Create a worker for running operations. * 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. * @param {function(MessageEvent): void} onMessage Called with a message event.
* @return {Worker} The worker. * @return {Worker} The worker.
*/ */
@@ -177,11 +186,22 @@ function createFauxWorker(config, onMessage) {
}; };
} }
/**
* @typedef {function(Error, ImageData, (Object|Array<Object>)): void} JobCallback
*/
/**
* @typedef {Object} Job
* @property {Object} meta Job metadata.
* @property {Array<ImageData>} inputs Array of input data.
* @property {JobCallback} callback Called when the job is complete.
*/
/** /**
* @typedef {Object} ProcessorOptions * @typedef {Object} ProcessorOptions
* @property {number} threads Number of workers to spawn. * @property {number} threads Number of workers to spawn.
* @property {function(Array, Object):*} operation The operation. * @property {Operation} operation The operation.
* @property {Object} [lib] Functions that will be made available to operations run in a worker. * @property {Object<string, Function>} [lib] Functions that will be made available to operations run in a worker.
* @property {number} queue The number of queued jobs to allow. * @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. * @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 { } else {
threads = config.threads || 1; threads = config.threads || 1;
} }
const workers = [];
/**
* @type {Array<Worker>}
*/
const workers = new Array(threads);
if (threads) { if (threads) {
for (let i = 0; i < threads; ++i) { for (let i = 0; i < threads; ++i) {
workers[i] = createWorker(config, this._onWorkerMessage.bind(this, i)); workers[i] = createWorker(config, this._onWorkerMessage.bind(this, i));
@@ -218,17 +242,32 @@ export class Processor extends Disposable {
); );
} }
this._workers = workers; this._workers = workers;
/**
* @type {Array<Job>}
* @private
*/
this._queue = []; this._queue = [];
this._maxQueueLength = config.queue || Infinity; this._maxQueueLength = config.queue || Infinity;
this._running = 0; this._running = 0;
/**
* @type {Object<number, any>}
* @private
*/
this._dataLookup = {}; this._dataLookup = {};
/**
* @type {Job}
* @private
*/
this._job = null; this._job = null;
} }
/** /**
* Run operation on input data. * Run operation on input data.
* @param {Array<Array|ImageData>} inputs Array of pixels or image data * @param {Array<ImageData>} inputs Array of image data.
* (depending on the operation type).
* @param {Object} meta A user data object. This is passed to all operations * @param {Object} meta A user data object. This is passed to all operations
* and must be serializable. * and must be serializable.
* @param {function(Error, ImageData, Object): void} callback Called when work * @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. * Add a job to the queue.
* @param {Object} job The job. * @param {Job} job The job.
*/ */
_enqueue(job) { _enqueue(job) {
this._queue.push(job); this._queue.push(job);
@@ -259,7 +298,10 @@ export class Processor extends Disposable {
* Dispatch a job. * Dispatch a job.
*/ */
_dispatch() { _dispatch() {
if (this._running === 0 && this._queue.length > 0) { if (this._running || this._queue.length === 0) {
return;
}
const job = this._queue.shift(); const job = this._queue.shift();
this._job = job; this._job = job;
const width = job.inputs[0].width; const width = job.inputs[0].width;
@@ -280,7 +322,9 @@ export class Processor extends Disposable {
}, },
buffers buffers
); );
} else { return;
}
const length = job.inputs[0].data.length; const length = job.inputs[0].data.length;
const segmentLength = 4 * Math.ceil(length / 4 / threads); const segmentLength = 4 * Math.ceil(length / 4 / threads);
for (let i = 0; i < threads; ++i) { for (let i = 0; i < threads; ++i) {
@@ -301,8 +345,6 @@ export class Processor extends Disposable {
); );
} }
} }
}
}
/** /**
* Handle messages from the worker. * Handle messages from the worker.
@@ -334,7 +376,7 @@ export class Processor extends Disposable {
} else { } else {
const length = job.inputs[0].data.length; const length = job.inputs[0].data.length;
data = new Uint8ClampedArray(length); data = new Uint8ClampedArray(length);
meta = new Array(length); meta = new Array(threads);
const segmentLength = 4 * Math.ceil(length / 4 / threads); const segmentLength = 4 * Math.ceil(length / 4 / threads);
for (let i = 0; i < threads; ++i) { for (let i = 0; i < threads; ++i) {
const buffer = this._dataLookup[i]['buffer']; const buffer = this._dataLookup[i]['buffer'];
@@ -388,14 +430,17 @@ export class Processor extends Disposable {
*/ */
const RasterEventType = { 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 * @event module:ol/source/Raster.RasterSourceEvent#beforeoperations
* @api * @api
*/ */
BEFOREOPERATIONS: 'beforeoperations', 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 * @event module:ol/source/Raster.RasterSourceEvent#afteroperations
* @api * @api
*/ */
@@ -424,7 +469,8 @@ export class RasterSourceEvent extends Event {
/** /**
* @param {string} type Type. * @param {string} type Type.
* @param {import("../PluggableMap.js").FrameState} frameState The frame state. * @param {import("../PluggableMap.js").FrameState} frameState The frame state.
* @param {Object} data An object made available to operations. * @param {Object|Array<Object>} 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) { constructor(type, frameState, data) {
super(type); super(type);
@@ -776,7 +822,7 @@ class RasterSource extends ImageSource {
* @param {import("../PluggableMap.js").FrameState} frameState The frame state. * @param {import("../PluggableMap.js").FrameState} frameState The frame state.
* @param {Error} err Any error during processing. * @param {Error} err Any error during processing.
* @param {ImageData} output The output image data. * @param {ImageData} output The output image data.
* @param {Object} data The user data. * @param {Object|Array<Object>} data The user data (or an array if more than one thread).
* @private * @private
*/ */
onWorkerComplete_(frameState, err, output, data) { onWorkerComplete_(frameState, err, output, data) {

View File

@@ -26,7 +26,7 @@ const green =
'AABAAEAAAICRAEAOw=='; 'AABAAEAAAICRAEAOw==';
where('Uint8ClampedArray').describe('ol.source.Raster', function () { where('Uint8ClampedArray').describe('ol.source.Raster', function () {
let map, target, redSource, greenSource, blueSource, raster; let map, target, redSource, greenSource, blueSource, layer, raster;
beforeEach(function () { beforeEach(function () {
target = document.createElement('div'); target = document.createElement('div');
@@ -73,6 +73,10 @@ where('Uint8ClampedArray').describe('ol.source.Raster', function () {
}, },
}); });
layer = new ImageLayer({
source: raster,
});
map = new Map({ map = new Map({
target: target, target: target,
view: new View({ view: new View({
@@ -83,11 +87,7 @@ where('Uint8ClampedArray').describe('ol.source.Raster', function () {
extent: extent, extent: extent,
}), }),
}), }),
layers: [ layers: [layer],
new ImageLayer({
source: raster,
}),
],
}); });
}); });
@@ -366,6 +366,32 @@ where('Uint8ClampedArray').describe('ol.source.Raster', function () {
view.setCenter([0, 0]); view.setCenter([0, 0]);
view.setZoom(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 () { describe('tile loading', function () {