Merge pull request #12667 from tschaub/raster-updates

Additional docs and type checking for raster source
This commit is contained in:
Tim Schaub
2021-08-26 16:44:21 -06:00
committed by GitHub
3 changed files with 158 additions and 86 deletions
+2 -2
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);
+124 -78
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,40 +90,40 @@ 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 arrays = new Array(numBuffers);
const pixels = new Array(numBuffers); const output = new Uint8ClampedArray(numBytes);
for (b = 0; b < numBuffers; ++b) { const arrays = new Array(numBuffers);
arrays[b] = new Uint8ClampedArray(buffers[b]); const pixels = new Array(numBuffers);
pixels[b] = [0, 0, 0, 0]; for (let b = 0; b < numBuffers; ++b) {
} arrays[b] = new Uint8ClampedArray(buffers[b]);
for (let i = 0; i < numBytes; i += 4) { pixels[b] = [0, 0, 0, 0];
for (let j = 0; j < numBuffers; ++j) { }
const array = arrays[j]; for (let i = 0; i < numBytes; i += 4) {
pixels[j][0] = array[i]; for (let j = 0; j < numBuffers; ++j) {
pixels[j][1] = array[i + 1]; const array = arrays[j];
pixels[j][2] = array[i + 2]; pixels[j][0] = array[i];
pixels[j][3] = array[i + 3]; pixels[j][1] = array[i + 1];
} pixels[j][2] = array[i + 2];
const pixel = operation(pixels, meta); pixels[j][3] = array[i + 3];
output[i] = pixel[0];
output[i + 1] = pixel[1];
output[i + 2] = pixel[2];
output[i + 3] = pixel[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; return output.buffer;
}; };
@@ -122,7 +131,7 @@ function createMinion(operation) {
/** /**
* 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,48 +298,51 @@ 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) {
const job = this._queue.shift(); return;
this._job = job; }
const width = job.inputs[0].width;
const height = job.inputs[0].height; const job = this._queue.shift();
const buffers = job.inputs.map(function (input) { this._job = job;
return input.data.buffer; const width = job.inputs[0].width;
}); const height = job.inputs[0].height;
const threads = this._workers.length; const buffers = job.inputs.map(function (input) {
this._running = threads; return input.data.buffer;
if (threads === 1) { });
this._workers[0].postMessage( const threads = this._workers.length;
{ this._running = threads;
buffers: buffers, if (threads === 1) {
meta: job.meta, this._workers[0].postMessage(
imageOps: this._imageOps, {
width: width, buffers: buffers,
height: height, meta: job.meta,
}, imageOps: this._imageOps,
buffers width: width,
); height: height,
} else { },
const length = job.inputs[0].data.length; buffers
const segmentLength = 4 * Math.ceil(length / 4 / threads); );
for (let i = 0; i < threads; ++i) { return;
const offset = i * segmentLength; }
const slices = [];
for (let j = 0, jj = buffers.length; j < jj; ++j) { const length = job.inputs[0].data.length;
slices.push(buffers[j].slice(offset, offset + segmentLength)); const segmentLength = 4 * Math.ceil(length / 4 / threads);
} for (let i = 0; i < threads; ++i) {
this._workers[i].postMessage( const offset = i * segmentLength;
{ const slices = [];
buffers: slices, for (let j = 0, jj = buffers.length; j < jj; ++j) {
meta: job.meta, slices.push(buffers[j].slice(offset, offset + segmentLength));
imageOps: this._imageOps,
width: width,
height: height,
},
slices
);
}
} }
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 { } 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) {
+32 -6
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 () {