// 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;
/**
* Flag to know if the current position is valid.
*
* @private
* @type {boolean}
*/
var isValidPosition = true;
/**
* 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};
/**
* The view offset.
*
* @private
* @type {object}
*/
var viewOffset = {x: 0, y: 0};
/**
* The zoom offset.
*
* @private
* @type {object}
*/
var zoomOffset = {x: 0, y: 0};
/**
* The flip offset.
*
* @private
* @type {object}
*/
var flipOffset = {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();
/**
* Image smoothing flag.
* see: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/imageSmoothingEnabled
*
* @private
* @type {boolean}
*/
var imageSmoothingEnabled = false;
/**
* Set the imageSmoothingEnabled flag value.
*
* @param {boolean} flag True to enable smoothing.
*/
this.enableImageSmoothing = function (flag) {
imageSmoothingEnabled = flag;
};
/**
* Set the associated view.
*
* @param {object} view The view.
* @param {number} index The associated data index.
*/
this.setView = function (view, index) {
dataIndex = index;
// 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, index);
};
/**
* 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 set event.
*
* @param {object} event The event.
*/
this.onimageset = function (event) {
// event.value = [index, image]
if (dataIndex === event.dataid) {
viewController.setImage(event.value[0], dataIndex);
setBaseSize(viewController.getImageSize().get2D());
needsDataUpdate = true;
}
};
/**
* Handle an image change event.
*
* @param {object} event The event.
*/
this.onimagechange = function (event) {
// event.value = [index]
if (dataIndex === event.dataid) {
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 layer base size (without scale).
*
* @returns {object} The size as {x,y}.
*/
this.getBaseSize = function () {
return baseSize;
};
/**
* Get the image world (mm) 2D size.
*
* @returns {object} The 2D size as {x,y}.
*/
this.getImageWorldSize = function () {
return viewController.getImageWorldSize();
};
/**
* 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) {
if (alpha === opacity) {
return;
}
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);
};
/**
* Add a flip offset along the layer X axis.
*/
this.addFlipOffsetX = function () {
// flip scale is handled by layer group
// flip offset
flipOffset.x += canvas.width / scale.x;
offset.x += flipOffset.x;
};
/**
* Add a flip offset along the layer Y axis.
*/
this.addFlipOffsetY = function () {
// flip scale is handled by layer group
// flip offset
flipOffset.y += canvas.height / scale.y;
offset.y += flipOffset.y;
};
/**
* Set the layer scale.
*
* @param {object} newScale The scale as {x,y}.
* @param {dwv.math.Point3D} center The scale center.
*/
this.setScale = function (newScale, center) {
var helper = viewController.getPlaneHelper();
var orientedNewScale = helper.getTargetOrientedPositiveXYZ(newScale);
var finalNewScale = {
x: fitScale.x * orientedNewScale.x,
y: fitScale.y * orientedNewScale.y
};
if (Math.abs(newScale.x) === 1 &&
Math.abs(newScale.y) === 1 &&
Math.abs(newScale.z) === 1) {
// reset zoom offset for scale=1
var resetOffset = {
x: offset.x - zoomOffset.x,
y: offset.y - zoomOffset.y
};
// store new offset
zoomOffset = {x: 0, y: 0};
offset = resetOffset;
} else {
if (typeof center !== 'undefined') {
var worldCenter = helper.getPlaneOffsetFromOffset3D({
x: center.getX(),
y: center.getY(),
z: center.getZ()
});
// center was obtained with viewLayer.displayToMainPlanePos
// compensated for baseOffset
// TODO: justify...
worldCenter = {
x: worldCenter.x + baseOffset.x,
y: worldCenter.y + baseOffset.y
};
var newOffset = dwv.gui.getScaledOffset(
offset, scale, finalNewScale, worldCenter);
var newZoomOffset = {
x: zoomOffset.x + newOffset.x - offset.x,
y: zoomOffset.y + newOffset.y - offset.y
};
// store new offset
zoomOffset = newZoomOffset;
offset = newOffset;
}
}
// store new scale
scale = finalNewScale;
};
/**
* Set the base layer offset. Updates the layer offset.
*
* @param {dwv.math.Vector3D} scrollOffset The scroll offset vector.
* @param {dwv.math.Vector3D} planeOffset The plane offset vector.
* @returns {boolean} True if the offset was updated.
*/
this.setBaseOffset = function (scrollOffset, planeOffset) {
var helper = viewController.getPlaneHelper();
var scrollIndex = helper.getNativeScrollIndex();
var newOffset = helper.getPlaneOffsetFromOffset3D({
x: scrollIndex === 0 ? scrollOffset.getX() : planeOffset.getX(),
y: scrollIndex === 1 ? scrollOffset.getY() : planeOffset.getY(),
z: scrollIndex === 2 ? scrollOffset.getZ() : planeOffset.getZ(),
});
var needsUpdate = baseOffset.x !== newOffset.x ||
baseOffset.y !== newOffset.y;
// reset offset if needed
if (needsUpdate) {
offset = {
x: offset.x - baseOffset.x + newOffset.x,
y: offset.y - baseOffset.y + newOffset.y
};
baseOffset = newOffset;
}
return needsUpdate;
};
/**
* 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: planeNewOffset.x +
viewOffset.x + baseOffset.x + zoomOffset.x + flipOffset.x,
y: planeNewOffset.y +
viewOffset.y + baseOffset.y + zoomOffset.y + flipOffset.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)
]);
};
/**
* Remove scale from a display position.
*
* @param {number} x The X position.
* @param {number} y The Y position.
* @returns {object} The de-scaled position as {x,y}.
*/
this.displayToPlaneScale = function (x, y) {
return {
x: x / scale.x,
y: y / scale.y
};
};
/**
* Get a plane position from a display position.
*
* @param {number} x The X position.
* @param {number} y The Y position.
* @returns {object} The plane position as {x,y}.
*/
this.displayToPlanePos = function (x, y) {
var deScaled = this.displayToPlaneScale(x, y);
return {
x: deScaled.x + offset.x,
y: deScaled.y + offset.y
};
};
this.planePosToDisplay = function (x, y) {
return {
x: (x - offset.x + baseOffset.x) * scale.x,
y: (y - offset.y + baseOffset.y) * scale.y
};
};
/**
* Get a main plane position from a display position.
*
* @param {number} x The X position.
* @param {number} y The Y position.
* @returns {object} The main plane position as {x,y}.
*/
this.displayToMainPlanePos = function (x, y) {
var planePos = this.displayToPlanePos(x, y);
return {
x: planePos.x - baseOffset.x,
y: planePos.y - baseOffset.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 () {
// skip for non valid position
if (!isValidPosition) {
return;
}
/**
* Render start event.
*
* @event dwv.App#renderstart
* @type {object}
* @property {string} type The event type.
*/
var event = {
type: 'renderstart',
layerid: this.getId(),
dataid: this.getDataIndex()
};
fireEvent(event);
// update data if needed
if (needsDataUpdate) {
updateImageData();
}
// context opacity
context.globalAlpha = opacity;
// clear context
this.clear();
// 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 = imageSmoothingEnabled;
// 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(),
dataid: this.getDataIndex()
};
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} alpha The initial data opacity.
*/
this.initialise = function (size, spacing, alpha) {
// set locals
baseSpacing = spacing;
opacity = Math.min(Math.max(alpha, 0), 1);
// create canvas
// (canvas size is set in fitToContainer)
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;
}
// off screen canvas
offscreenCanvas = document.createElement('canvas');
// set base size: needs an existing context and off screen canvas
setBaseSize(size);
// update data on first draw
needsDataUpdate = true;
};
/**
* Set the base size of the layer.
*
* @param {object} size The size as {x,y}.
*/
function setBaseSize(size) {
// check canvas creation
if (!dwv.gui.canCreateCanvas(size.x, size.y)) {
throw new Error('Cannot create canvas with size ' +
size.x + ', ' + size.y);
}
// set local
baseSize = size;
// off screen 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);
}
/**
* Fit the layer to its parent container.
*
* @param {number} fitScale1D The 1D fit scale.
* @param {object} fitSize The fit size as {x,y}.
* @param {object} fitOffset The fit offset as {x,y}.
*/
this.fitToContainer = function (fitScale1D, fitSize, fitOffset) {
var needsDraw = false;
// update canvas size if needed (triggers canvas reset)
if (canvas.width !== fitSize.x || canvas.height !== fitSize.y) {
if (!dwv.gui.canCreateCanvas(fitSize.x, fitSize.y)) {
throw new Error('Cannot resize canvas ' + fitSize.x + ', ' + fitSize.y);
}
// canvas size change triggers canvas reset
canvas.width = fitSize.x;
canvas.height = fitSize.y;
// update draw flag
needsDraw = true;
}
// previous scale without fit
var previousScale = {
x: scale.x / fitScale.x,
y: scale.y / fitScale.y
};
// fit scale
var newFitScale = {
x: fitScale1D * baseSpacing.x,
y: fitScale1D * baseSpacing.y
};
// scale
var newScale = {
x: previousScale.x * newFitScale.x,
y: previousScale.y * newFitScale.y
};
// check if different
if (previousScale.x !== newScale.x || previousScale.y !== newScale.y) {
fitScale = newFitScale;
scale = newScale;
// update draw flag
needsDraw = true;
}
// view offset
var newViewOffset = {
x: fitOffset.x / newFitScale.x,
y: fitOffset.y / newFitScale.y
};
// check if different
if (viewOffset.x !== newViewOffset.x || viewOffset.y !== newViewOffset.y) {
viewOffset = newViewOffset;
offset = {
x: viewOffset.x + baseOffset.x + zoomOffset.x + flipOffset.x,
y: viewOffset.y + baseOffset.y + zoomOffset.y + flipOffset.y
};
// update draw flag
needsDraw = true;
}
// draw if needed
if (needsDraw) {
this.draw();
}
};
/**
* 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, {passive: true});
}
};
/**
* 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.dataid = 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
var skip = typeof event.skipGenerate !== 'undefined' &&
event.skipGenerate === true;
if (!skip) {
needsDataUpdate = true;
self.draw();
}
}
/**
* Handle colour map change.
*
* @param {object} _event The event fired when changing the colour map.
* @private
*/
function onColourChange(_event) {
var skip = typeof event.skipGenerate !== 'undefined' &&
event.skipGenerate === true;
if (!skip) {
needsDataUpdate = true;
self.draw();
}
}
/**
* Handle position change.
*
* @param {object} event The event fired when changing the position.
* @private
*/
function onPositionChange(event) {
var skip = typeof event.skipGenerate !== 'undefined' &&
event.skipGenerate === true;
if (!skip) {
var valid = true;
if (typeof event.valid !== 'undefined') {
valid = event.valid;
}
// clear for non valid events
if (!valid) {
// clear only once
if (isValidPosition) {
isValidPosition = false;
self.clear();
}
} else {
// 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 || !isValidPosition) {
// reset valid flag
isValidPosition = true;
// reset update flag
needsDataUpdate = true;
self.draw();
}
}
}
}
/**
* Handle alpha function change.
*
* @param {object} event The event fired when changing the function.
* @private
*/
function onAlphaFuncChange(event) {
var skip = typeof event.skipGenerate !== 'undefined' &&
event.skipGenerate === true;
if (!skip) {
needsDataUpdate = true;
self.draw();
}
}
/**
* Set the current position.
*
* @param {dwv.math.Point} position The new position.
* @param {dwv.math.Index} _index The new index.
* @returns {boolean} True if the position was updated.
*/
this.setCurrentPosition = function (position, _index) {
return viewController.setCurrentPosition(position);
};
/**
* Clear the context.
*/
this.clear = function () {
// 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();
};
/**
* 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