// namespaces
var dwv = dwv || {};
dwv.gui = dwv.gui || {};
/**
* View layer.
*
* @param {object} containerDiv The layer div, its id will be used
* as this layer id.
* @class
*/
dwv.gui.ViewLayer = function (containerDiv) {
// specific css class name
containerDiv.className += ' viewLayer';
// closure to self
var self = this;
/**
* The view controller.
*
* @private
* @type {object}
*/
var viewController = null;
/**
* The main display canvas.
*
* @private
* @type {object}
*/
var canvas = null;
/**
* The offscreen canvas: used to store the raw, unscaled pixel data.
*
* @private
* @type {object}
*/
var offscreenCanvas = null;
/**
* The associated CanvasRenderingContext2D.
*
* @private
* @type {object}
*/
var context = null;
/**
* The image data array.
*
* @private
* @type {Array}
*/
var imageData = null;
/**
* The layer base size as {x,y}.
*
* @private
* @type {object}
*/
var baseSize;
/**
* The layer base spacing as {x,y}.
*
* @private
* @type {object}
*/
var baseSpacing;
/**
* The layer opacity.
*
* @private
* @type {number}
*/
var opacity = 1;
/**
* The layer scale.
*
* @private
* @type {object}
*/
var scale = {x: 1, y: 1};
/**
* The layer fit scale.
*
* @private
* @type {object}
*/
var fitScale = {x: 1, y: 1};
/**
* The layer offset.
*
* @private
* @type {object}
*/
var offset = {x: 0, y: 0};
/**
* The base layer offset.
*
* @private
* @type {object}
*/
var baseOffset = {x: 0, y: 0};
/**
* Data update flag.
*
* @private
* @type {boolean}
*/
var needsDataUpdate = null;
/**
* The associated data index.
*
* @private
* @type {number}
*/
var dataIndex = null;
/**
* Get the associated data index.
*
* @returns {number} The index.
*/
this.getDataIndex = function () {
return dataIndex;
};
/**
* Listener handler.
*
* @private
* @type {object}
*/
var listenerHandler = new dwv.utils.ListenerHandler();
/**
* Set the associated view.
*
* @param {object} view The view.
*/
this.setView = function (view) {
// local listeners
view.addEventListener('wlchange', onWLChange);
view.addEventListener('colourchange', onColourChange);
view.addEventListener('positionchange', onPositionChange);
view.addEventListener('alphafuncchange', onAlphaFuncChange);
// view events
for (var j = 0; j < dwv.image.viewEventNames.length; ++j) {
view.addEventListener(dwv.image.viewEventNames[j], fireEvent);
}
// create view controller
viewController = new dwv.ctrl.ViewController(view);
};
/**
* Get the view controller.
*
* @returns {object} The controller.
*/
this.getViewController = function () {
return viewController;
};
/**
* Get the canvas image data.
*
* @returns {object} The image data.
*/
this.getImageData = function () {
return imageData;
};
/**
* Handle an image change event.
*
* @param {object} event The event.
*/
this.onimagechange = function (event) {
// event.value = [index, image]
if (dataIndex === event.value[0]) {
viewController.setImage(event.value[1]);
needsDataUpdate = true;
}
};
// common layer methods [start] ---------------
/**
* Get the id of the layer.
*
* @returns {string} The string id.
*/
this.getId = function () {
return containerDiv.id;
};
/**
* Get the data full size, ie size * spacing.
*
* @returns {object} The full size as {x,y}.
*/
this.getFullSize = function () {
return {
x: baseSize.x * baseSpacing.x,
y: baseSize.y * baseSpacing.y
};
};
/**
* Get the layer base size (without scale).
*
* @returns {object} The size as {x,y}.
*/
this.getBaseSize = function () {
return baseSize;
};
/**
* Get the layer opacity.
*
* @returns {number} The opacity ([0:1] range).
*/
this.getOpacity = function () {
return opacity;
};
/**
* Set the layer opacity.
*
* @param {number} alpha The opacity ([0:1] range).
*/
this.setOpacity = function (alpha) {
opacity = Math.min(Math.max(alpha, 0), 1);
/**
* Opacity change event.
*
* @event dwv.App#opacitychange
* @type {object}
* @property {string} type The event type.
*/
var event = {
type: 'opacitychange',
value: [opacity]
};
fireEvent(event);
};
/**
* Set the layer scale.
*
* @param {object} newScale The scale as {x,y}.
*/
this.setScale = function (newScale) {
var helper = viewController.getPlaneHelper();
var orientedNewScale = helper.getOrientedXYZ(newScale);
scale = {
x: fitScale.x * orientedNewScale.x,
y: fitScale.y * orientedNewScale.y
};
};
/**
* Set the base layer offset. Resets the layer offset.
*
* @param {object} off The offset as {x,y}.
*/
this.setBaseOffset = function (off) {
var helper = viewController.getPlaneHelper();
baseOffset = helper.getPlaneOffsetFromOffset3D({
x: off.getX(),
y: off.getY(),
z: off.getZ()
});
// reset offset
offset = baseOffset;
};
/**
* Set the layer offset.
*
* @param {object} newOffset The offset as {x,y}.
*/
this.setOffset = function (newOffset) {
var helper = viewController.getPlaneHelper();
var planeNewOffset = helper.getPlaneOffsetFromOffset3D(newOffset);
offset = {
x: baseOffset.x + planeNewOffset.x,
y: baseOffset.y + planeNewOffset.y
};
};
/**
* Transform a display position to an index.
*
* @param {number} x The X position.
* @param {number} y The Y position.
* @returns {dwv.math.Index} The equivalent index.
*/
this.displayToPlaneIndex = function (x, y) {
var planePos = this.displayToPlanePos(x, y);
return new dwv.math.Index([
Math.floor(planePos.x),
Math.floor(planePos.y)
]);
};
this.displayToPlaneScale = function (x, y) {
return {
x: x / scale.x,
y: y / scale.y
};
};
this.displayToPlanePos = function (x, y) {
var deScaled = this.displayToPlaneScale(x, y);
return {
x: deScaled.x + offset.x,
y: deScaled.y + offset.y
};
};
/**
* Display the layer.
*
* @param {boolean} flag Whether to display the layer or not.
*/
this.display = function (flag) {
containerDiv.style.display = flag ? '' : 'none';
};
/**
* Check if the layer is visible.
*
* @returns {boolean} True if the layer is visible.
*/
this.isVisible = function () {
return containerDiv.style.display === '';
};
/**
* Draw the content (imageData) of the layer.
* The imageData variable needs to be set
*
* @fires dwv.App#renderstart
* @fires dwv.App#renderend
*/
this.draw = function () {
/**
* Render start event.
*
* @event dwv.App#renderstart
* @type {object}
* @property {string} type The event type.
*/
var event = {
type: 'renderstart',
layerid: this.getId()
};
fireEvent(event);
// update data if needed
if (needsDataUpdate) {
updateImageData();
}
// context opacity
context.globalAlpha = opacity;
// clear the context: reset the transform first
// store the current transformation matrix
context.save();
// use the identity matrix while clearing the canvas
context.setTransform(1, 0, 0, 1, 0, 0);
context.clearRect(0, 0, canvas.width, canvas.height);
// restore the transform
context.restore();
// draw the cached canvas on the context
// transform takes as input a, b, c, d, e, f to create
// the transform matrix (column-major order):
// [ a c e ]
// [ b d f ]
// [ 0 0 1 ]
context.setTransform(
scale.x,
0,
0,
scale.y,
-1 * offset.x * scale.x,
-1 * offset.y * scale.y
);
// disable smoothing (set just before draw, could be reset by resize)
context.imageSmoothingEnabled = false;
// draw image
context.drawImage(offscreenCanvas, 0, 0);
/**
* Render end event.
*
* @event dwv.App#renderend
* @type {object}
* @property {string} type The event type.
*/
event = {
type: 'renderend',
layerid: this.getId()
};
fireEvent(event);
};
/**
* Initialise the layer: set the canvas and context
*
* @param {object} size The image size as {x,y}.
* @param {object} spacing The image spacing as {x,y}.
* @param {number} index The associated data index.
*/
this.initialise = function (size, spacing, index) {
// set locals
baseSize = size;
baseSpacing = spacing;
dataIndex = index;
// create canvas
canvas = document.createElement('canvas');
containerDiv.appendChild(canvas);
// check that the getContext method exists
if (!canvas.getContext) {
alert('Error: no canvas.getContext method.');
return;
}
// get the 2D context
context = canvas.getContext('2d');
if (!context) {
alert('Error: failed to get the 2D context.');
return;
}
// check canvas
if (!dwv.gui.canCreateCanvas(baseSize.x, baseSize.y)) {
throw new Error('Cannot create canvas ' + baseSize.x + ', ' + baseSize.y);
}
// canvas sizes
canvas.width = baseSize.x;
canvas.height = baseSize.y;
// off screen canvas
offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = baseSize.x;
offscreenCanvas.height = baseSize.y;
// original empty image data array
context.clearRect(0, 0, baseSize.x, baseSize.y);
imageData = context.createImageData(baseSize.x, baseSize.y);
// update data on first draw
needsDataUpdate = true;
};
/**
* Fit the layer to its parent container.
*
* @param {number} fitScale1D The 1D fit scale.
*/
this.fitToContainer = function (fitScale1D) {
// update fit scale
fitScale = {
x: fitScale1D * baseSpacing.x,
y: fitScale1D * baseSpacing.y
};
// update canvas
var fullSize = this.getFullSize();
var width = Math.floor(fullSize.x * fitScale1D);
var height = Math.floor(fullSize.y * fitScale1D);
if (!dwv.gui.canCreateCanvas(width, height)) {
throw new Error('Cannot resize canvas ' + width + ', ' + height);
}
canvas.width = width;
canvas.height = height;
// reset scale
this.setScale({x: 1, y: 1, z: 1});
};
/**
* Enable and listen to container interaction events.
*/
this.bindInteraction = function () {
// allow pointer events
containerDiv.style.pointerEvents = 'auto';
// interaction events
var names = dwv.gui.interactionEventNames;
for (var i = 0; i < names.length; ++i) {
containerDiv.addEventListener(names[i], fireEvent);
}
};
/**
* Disable and stop listening to container interaction events.
*/
this.unbindInteraction = function () {
// disable pointer events
containerDiv.style.pointerEvents = 'none';
// interaction events
var names = dwv.gui.interactionEventNames;
for (var i = 0; i < names.length; ++i) {
containerDiv.removeEventListener(names[i], fireEvent);
}
};
/**
* Add an event listener to this class.
*
* @param {string} type The event type.
* @param {object} callback The method associated with the provided
* event type, will be called with the fired event.
*/
this.addEventListener = function (type, callback) {
listenerHandler.add(type, callback);
};
/**
* Remove an event listener from this class.
*
* @param {string} type The event type.
* @param {object} callback The method associated with the provided
* event type.
*/
this.removeEventListener = function (type, callback) {
listenerHandler.remove(type, callback);
};
/**
* Fire an event: call all associated listeners with the input event object.
*
* @param {object} event The event to fire.
* @private
*/
function fireEvent(event) {
event.srclayerid = self.getId();
event.dataindex = dataIndex;
listenerHandler.fireEvent(event);
}
// common layer methods [end] ---------------
/**
* Update the canvas image data.
*/
function updateImageData() {
// generate image data
viewController.generateImageData(imageData);
// pass the data to the off screen canvas
offscreenCanvas.getContext('2d').putImageData(imageData, 0, 0);
// update data flag
needsDataUpdate = false;
}
/**
* Handle window/level change.
*
* @param {object} event The event fired when changing the window/level.
* @private
*/
function onWLChange(event) {
// generate and draw if no skip flag
if (typeof event.skipGenerate === 'undefined' ||
event.skipGenerate === false) {
needsDataUpdate = true;
self.draw();
}
}
/**
* Handle colour map change.
*
* @param {object} _event The event fired when changing the colour map.
* @private
*/
function onColourChange(_event) {
needsDataUpdate = true;
self.draw();
}
/**
* Handle position change.
*
* @param {object} event The event fired when changing the position.
* @private
*/
function onPositionChange(event) {
if (typeof event.skipGenerate === 'undefined' ||
event.skipGenerate === false) {
// 3D dimensions
var dims3D = [0, 1, 2];
// remove scroll index
var indexScrollIndex = dims3D.indexOf(viewController.getScrollIndex());
dims3D.splice(indexScrollIndex, 1);
// remove non scroll index from diff dims
var diffDims = event.diffDims.filter(function (item) {
return dims3D.indexOf(item) === -1;
});
// update if we have something left
if (diffDims.length !== 0) {
needsDataUpdate = true;
self.draw();
}
}
}
/**
* Handle alpha function change.
*
* @param {object} event The event fired when changing the function.
* @private
*/
function onAlphaFuncChange(event) {
if (typeof event.skipGenerate === 'undefined' ||
event.skipGenerate === false) {
needsDataUpdate = true;
self.draw();
}
}
/**
* Set the current position.
*
* @param {dwv.math.Point} position The new position.
* @param {dwv.math.Index} _index The new index.
*/
this.setCurrentPosition = function (position, _index) {
viewController.setCurrentPosition(position);
};
/**
* Clear the context and reset the image data.
*/
this.clear = function () {
context.clearRect(0, 0, canvas.width, canvas.height);
imageData = context.getImageData(0, 0, canvas.width, canvas.height);
this.resetLayout();
};
/**
* Align on another layer.
*
* @param {dwv.gui.ViewLayer} rhs The layer to align on.
*/
this.align = function (rhs) {
canvas.style.top = rhs.getCanvas().offsetTop;
canvas.style.left = rhs.getCanvas().offsetLeft;
};
}; // ViewLayer class