CompositeImageBuilder

CompositeImageBuilder

This is a builder which creates an ImageBuilder that lets you process composite
datasets. The implementation relies on a single off-screen canvas to generate the
resulting image of a composite structure (rgb.jpg + composite.json).

var CompositeImageBuilder = require('paraviewweb/src/Rendering/Image/CompositeImageBuilder'),
instance = new CompositeImageBuilder(qdm, pm);

constructor(queryDataModel, pipelineModel)

Create an instance of a CompositeImageBuilder using the associated
queryDataModel that should be used to fetch the data.

And the pipelineModel that should be used for controlling the ImageBuilder.

Under the hood this will create an off-screen canvas for the image generation.
Then, depending if the method setPushMethodAsImage() has been called,
the ‘image-ready’ notification will not contain the same object.
By default we use the setPushMethodAsBuffer() configuration.

Below are the two event structures

// setPushMethodAsBuffer()
var eventAsBuffer = {
canvas: DOMElement,
imageData: ImageDataFromCanvas,
area: [0, 0, width, height],
outputSize: [width, height],
builder: this
};

// setPushMethodAsImage()
var eventAsImage = {
url: 'data:image/png:ASDGFsdfgsdgf...',
builder: this
};

update() - inherited

Trigger the fetching of the data (composite.json + rgb.jpg).

setPipelineQuery(pipelineQuery)

Should be called each time the pipeline setting is changed.

The pipelineQuery is a string that encodes the pipeline configuration such as
which layer is visible or not and which field should be rendered for a given layer.

The pipelineQuery is structured as follows:

var pipelineQuery = "A_BBCAD_EA";

// In that example we have the following setting
var layerSettings = [
"A_", // Layer A is invisible
"BB", // Layer B is using field B
"CA", // Layer C is using field A
"D_", // Layer D is invisible
"EA" // Layer E is using field A
];

render()

Process the current set of loaded data and render it into the background canvas.
Once done, an event gets triggered to let the application know that the image is
ready to be rendered/displayed somewhere.

onImageReady(callback) : subscription - inherited

Allows the registration of a callback(data, envelope) function when the
actual generated image is ready.

destroy()

Free the internal resources of the current instance.

* updateCompositeMap(query, composite)

Internal function used to update the composite map for faster rendering.

_updateOffsetMap(pipelineQuery)

Internal function used to update the offset map based on the Pipeline configuration.
The pipelineQuery is a String that encode the pipeline configuration such as
which layer is visible or not and which field should be rendered for a given layer.

_pushToFront(width, height)

Trigger the event notification that the image is ready. This will call the proper
method to either send the ImageData or an ImageURL.

_pushToFrontAsImage(width, height)

Called as pushToFront when setPushMethodAsImage() is used.

_pushToFrontAsBuffer(width, height)

Called as pushToFront when setPushMethodAsBuffer() is used.

getListeners - inherited

Returns a list of MouseHandler listeners.

getControlWidgets - inherited

Returns a list of Widgets needed to drive the ImageBuilder.

getControlModels - inherited

Returns a list of Model that can drive the ImageBuilder.

Source

index.js
import max from 'mout/object/max';

import AbstractImageBuilder from '../AbstractImageBuilder';
import CanvasOffscreenBuffer from '../../../Common/Misc/CanvasOffscreenBuffer';

export default class CompositeImageBuilder extends AbstractImageBuilder {
// ------------------------------------------------------------------------

constructor(queryDataModel, pipelineModel) {
super({
queryDataModel,
pipelineModel,
handleRecord: true,
dimensions: queryDataModel.originalData.CompositePipeline.dimensions,
});

this.metadata = queryDataModel.originalData.CompositePipeline;
this.compositeMap = {};
this.offsetMap = {};
this.spriteSize = max(this.metadata.offset);
this.query = null;
this.composite = null;

this.bgCanvas = new CanvasOffscreenBuffer(
this.metadata.dimensions[0],
this.metadata.dimensions[1]
);
this.registerObjectToFree(this.bgCanvas);

this.fgCanvas = null;

this.registerSubscription(
queryDataModel.onDataChange((data, envelope) => {
this.sprite = data.sprite;
this.composite = data.composite.data['pixel-order'].split('+');
this.updateCompositeMap(this.query, this.composite);
this.render();
})
);

this.registerSubscription(
this.pipelineModel.onChange((data, envelope) => {
this.setPipelineQuery(data);
})
);

this.setPipelineQuery(this.pipelineModel.getPipelineQuery());
}

// ------------------------------------------------------------------------

updateOffsetMap(query) {
const layers = this.metadata.layers;
const count = layers.length;
const offsets = this.metadata.offset;

this.offsetMap = {};
this.compositeMap = {};
for (let idx = 0; idx < count; idx++) {
const fieldCode = query[idx * 2 + 1];
if (fieldCode === '_') {
this.offsetMap[layers[idx]] = -1;
} else {
this.offsetMap[layers[idx]] =
this.spriteSize - offsets[layers[idx] + fieldCode];
}
}
}

// ------------------------------------------------------------------------

updateCompositeMap(query, composite) {
if (query === null || composite === null) {
return;
}
const compositeArray = composite;
const map = this.compositeMap;

let count = compositeArray.length;
while (count) {
count -= 1;
const key = compositeArray[count];
if (key[0] === '@') {
// Skip pixels
} else if ({}.hasOwnProperty.call(map, key)) {
// Already computed
} else {
let offset = -1;
for (let i = 0, size = key.length; i < size; i++) {
offset = this.offsetMap[key[i]];
if (offset !== -1) {
i = size;
}
}
map[key] = offset;
}
}
}

// ------------------------------------------------------------------------

pushToFrontAsImage(width, height) {
let ctx = null;

// Make sure we have a foreground buffer
if (this.fgCanvas) {
this.fgCanvas.size(width, height);
} else {
this.fgCanvas = new CanvasOffscreenBuffer(width, height);
this.registerObjectToFree(this.fgCanvas);
}

ctx = this.fgCanvas.get2DContext();
ctx.drawImage(this.bgCanvas.el, 0, 0, width, height, 0, 0, width, height);

const readyImage = {
url: this.fgCanvas.toDataURL(),
builder: this,
};

// Let everyone know the image is ready
this.imageReady(readyImage);
}

// ------------------------------------------------------------------------

pushToFrontAsBuffer(width, height) {
const readyImage = {
canvas: this.bgCanvas.el,
imageData: this.bgCanvas.el
.getContext('2d')
.getImageData(0, 0, width, height),
area: [0, 0, width, height],
outputSize: [width, height],
builder: this,
arguments: this.queryDataModel.getQuery(),
};

// Add pipeline info + ts
readyImage.arguments.pipeline = this.query;

// Let everyone know the image is ready
this.imageReady(readyImage);

// In case of exploration trigger next data fetch
this.queryDataModel.nextExploration();
}

// ------------------------------------------------------------------------

setPipelineQuery(query) {
if (this.query !== query) {
this.query = query;
this.updateOffsetMap(query);
this.updateCompositeMap(query, this.composite);
this.render();
}
}

// ------------------------------------------------------------------------

render() {
if (!this.sprite) {
this.queryDataModel.fetchData();
return;
}
if (this.query === null) {
return;
}

const ctx = this.bgCanvas.get2DContext();
const dimensions = this.metadata.dimensions;
const compositeArray = this.composite;
const count = compositeArray.length;
const modulo = dimensions[0];

let offset = 1;
let x = 0;
let y = 0;

function addToX(delta) {
x += delta;
y += Math.floor(x / modulo);
x %= modulo;
}

if (this.sprite.image.complete) {
// Free callback if any
if (this.sprite.image.onload) {
this.sprite.image.onload = null;
}

ctx.clearRect(0, 0, dimensions[0], dimensions[1]);
for (let idx = 0; idx < count; idx++) {
const key = compositeArray[idx];
if (key[0] === '@') {
// Shift (x,y)
addToX(Number(key.replace(/@/, '+')));
} else {
offset = this.compositeMap[key];
if (offset !== -1) {
ctx.drawImage(
this.sprite.image,
x,
y + dimensions[1] * offset,
1,
1,
x,
y,
1,
1
);
}
addToX(1);
}
}

this.pushToFrontAsBuffer(dimensions[0], dimensions[1]);
} else {
this.sprite.image.onload = () => {
this.render();
};
}
}

// ------------------------------------------------------------------------

destroy() {
super.destroy();

this.bgCanvas = null;
this.fgCanvas = null;
this.compositeMap = null;
this.offsetMap = null;
}
}