// namespaces
var dwv = dwv || {};
dwv.gui = dwv.gui || {};
/**
* Get the layer div id.
*
* @param {string} groupDivId The layer group div id.
* @param {number} layerId The lyaer id.
* @returns {string} A string id.
*/
dwv.gui.getLayerDivId = function (groupDivId, layerId) {
return groupDivId + '-layer-' + layerId;
};
/**
* Get the layer details from a div id.
*
* @param {string} idString The layer div id.
* @returns {object} The layer details as {groupDivId, layerId}.
*/
dwv.gui.getLayerDetailsFromLayerDivId = function (idString) {
var split = idString.split('-layer-');
if (split.length !== 2) {
dwv.logger.warn('Not the expected layer div id format...');
}
return {
groupDivId: split[0],
layerId: split[1]
};
};
/**
* Get the layer details from a mouse event.
*
* @param {object} event The event to get the layer div id from. Expecting
* an event origininating from a canvas inside a layer HTML div
* with the 'layer' class and id generated with `dwv.gui.getLayerDivId`.
* @returns {object} The layer details as {groupDivId, layerId}.
*/
dwv.gui.getLayerDetailsFromEvent = function (event) {
var res = null;
// get the closest element from the event target and with the 'layer' class
var layerDiv = event.target.closest('.layer');
if (layerDiv && typeof layerDiv.id !== 'undefined') {
res = dwv.gui.getLayerDetailsFromLayerDivId(layerDiv.id);
}
return res;
};
/**
* Get the view orientation according to an image and target orientation.
* The view orientation is used to go from target to image space.
*
* @param {dwv.math.Matrix33} imageOrientation The image geometry.
* @param {dwv.math.Matrix33} targetOrientation The target orientation.
* @returns {dwv.math.Matrix33} The view orientation.
*/
dwv.gui.getViewOrientation = function (imageOrientation, targetOrientation) {
var viewOrientation = dwv.math.getIdentityMat33();
if (typeof targetOrientation !== 'undefined') {
// i: image, v: view, t: target, O: orientation, P: point
// [Img] -- Oi --> [Real] <-- Ot -- [Target]
// Pi = (Oi)-1 * Ot * Pt = Ov * Pt
// -> Ov = (Oi)-1 * Ot
// TODO: asOneAndZeros simplifies but not nice...
viewOrientation =
imageOrientation.asOneAndZeros().getInverse().multiply(targetOrientation);
}
// TODO: why abs???
return viewOrientation.getAbs();
};
/**
* Get the target orientation according to an image and view orientation.
* The target orientation is used to go from target to real space.
*
* @param {dwv.math.Matrix33} imageOrientation The image geometry.
* @param {dwv.math.Matrix33} viewOrientation The view orientation.
* @returns {dwv.math.Matrix33} The target orientation.
*/
dwv.gui.getTargetOrientation = function (imageOrientation, viewOrientation) {
// i: image, v: view, t: target, O: orientation, P: point
// [Img] -- Oi --> [Real] <-- Ot -- [Target]
// Pi = (Oi)-1 * Ot * Pt = Ov * Pt
// -> Ot = Oi * Ov
// note: asOneAndZeros as in dwv.gui.getViewOrientation...
var targetOrientation =
imageOrientation.asOneAndZeros().multiply(viewOrientation);
// TODO: why abs???
var simpleImageOrientation = imageOrientation.asOneAndZeros().getAbs();
if (simpleImageOrientation.equals(dwv.math.getCoronalMat33().getAbs())) {
targetOrientation = targetOrientation.getAbs();
}
return targetOrientation;
};
/**
* Get a scaled offset to adapt to new scale and such as the input center
* stays at the same position.
*
* @param {object} offset The previous offset as {x,y}.
* @param {object} scale The previous scale as {x,y}.
* @param {object} newScale The new scale as {x,y}.
* @param {object} center The scale center as {x,y}.
* @returns {object} The scaled offset as {x,y}.
*/
dwv.gui.getScaledOffset = function (offset, scale, newScale, center) {
// worldPoint = indexPoint / scale + offset
//=> indexPoint = (worldPoint - offset ) * scale
// plane center should stay the same:
// indexCenter / newScale + newOffset =
// indexCenter / oldScale + oldOffset
//=> newOffset = indexCenter / oldScale + oldOffset -
// indexCenter / newScale
//=> newOffset = worldCenter - indexCenter / newScale
var indexCenter = {
x: (center.x - offset.x) * scale.x,
y: (center.y - offset.y) * scale.y
};
return {
x: center.x - (indexCenter.x / newScale.x),
y: center.y - (indexCenter.y / newScale.y)
};
};
/**
* Layer group.
*
* Display position: {x,y}
* Plane position: Index (access: get(i))
* (world) Position: Point3D (access: getX, getY, getZ)
*
* Display -> World:
* planePos = viewLayer.displayToPlanePos(displayPos)
* -> compensate for layer scale and offset
* pos = viewController.getPositionFromPlanePoint(planePos)
*
* World -> display
* planePos = viewController.getOffset3DFromPlaneOffset(pos)
* no need yet for a planePos to displayPos...
*
* @param {object} containerDiv The associated HTML div.
* @class
*/
dwv.gui.LayerGroup = function (containerDiv) {
// closure to self
var self = this;
// list of layers
var layers = [];
/**
* The layer scale as {x,y}.
*
* @private
* @type {object}
*/
var scale = {x: 1, y: 1, z: 1};
/**
* The base scale as {x,y}: all posterior scale will be on top of this one.
*
* @private
* @type {object}
*/
var baseScale = {x: 1, y: 1, z: 1};
/**
* The layer offset as {x,y}.
*
* @private
* @type {object}
*/
var offset = {x: 0, y: 0, z: 0};
/**
* Active view layer index.
*
* @private
* @type {number}
*/
var activeViewLayerIndex = null;
/**
* Active draw layer index.
*
* @private
* @type {number}
*/
var activeDrawLayerIndex = null;
/**
* Listener handler.
*
* @type {object}
* @private
*/
var listenerHandler = new dwv.utils.ListenerHandler();
/**
* The target orientation matrix.
*
* @type {object}
* @private
*/
var targetOrientation;
/**
* Flag to activate crosshair or not.
*
* @type {boolean}
* @private
*/
var showCrosshair = false;
/**
* The current position used for the crosshair.
*
* @type {dwv.math.Point}
* @private
*/
var currentPosition;
/**
* Get the target orientation.
*
* @returns {dwv.math.Matrix33} The orientation matrix.
*/
this.getTargetOrientation = function () {
return targetOrientation;
};
/**
* Set the target orientation.
*
* @param {dwv.math.Matrix33} orientation The orientation matrix.
*/
this.setTargetOrientation = function (orientation) {
targetOrientation = orientation;
};
/**
* Get the showCrosshair flag.
*
* @returns {boolean} True to display the crosshair.
*/
this.getShowCrosshair = function () {
return showCrosshair;
};
/**
* Set the showCrosshair flag.
*
* @param {boolean} flag True to display the crosshair.
*/
this.setShowCrosshair = function (flag) {
showCrosshair = flag;
if (flag) {
// listen to offset and zoom change
self.addEventListener('offsetchange', updateCrosshairOnChange);
self.addEventListener('zoomchange', updateCrosshairOnChange);
// show crosshair div
showCrosshairDiv();
} else {
// listen to offset and zoom change
self.removeEventListener('offsetchange', updateCrosshairOnChange);
self.removeEventListener('zoomchange', updateCrosshairOnChange);
// remove crosshair div
removeCrosshairDiv();
}
};
/**
* Update crosshair on offset or zoom change.
*/
function updateCrosshairOnChange() {
showCrosshairDiv();
}
/**
* Get the Id of the container div.
*
* @returns {string} The id of the div.
*/
this.getDivId = function () {
return containerDiv.id;
};
/**
* Get the layer scale.
*
* @returns {object} The scale as {x,y,z}.
*/
this.getScale = function () {
return scale;
};
/**
* Get the base scale.
*
* @returns {object} The scale as {x,y,z}.
*/
this.getBaseScale = function () {
return baseScale;
};
/**
* Get the added scale: the scale added to the base scale
*
* @returns {object} The scale as {x,y,z}.
*/
this.getAddedScale = function () {
return {
x: scale.x / baseScale.x,
y: scale.y / baseScale.y,
z: scale.z / baseScale.z
};
};
/**
* Get the layer offset.
*
* @returns {object} The offset as {x,y,z}.
*/
this.getOffset = function () {
return offset;
};
/**
* Get the number of layers handled by this class.
*
* @returns {number} The number of layers.
*/
this.getNumberOfLayers = function () {
return layers.length;
};
/**
* Get the active image layer.
*
* @returns {object} The layer.
*/
this.getActiveViewLayer = function () {
return layers[activeViewLayerIndex];
};
/**
* Get the view layers associated to a data index.
*
* @param {number} index The data index.
* @returns {Array} The layers.
*/
this.getViewLayersByDataIndex = function (index) {
var res = [];
for (var i = 0; i < layers.length; ++i) {
if (layers[i] instanceof dwv.gui.ViewLayer &&
layers[i].getDataIndex() === index) {
res.push(layers[i]);
}
}
return res;
};
/**
* Search view layers for equal imae meta data.
*
* @param {object} meta The meta data to find.
* @returns {Array} The list of view layers that contain matched data.
*/
this.searchViewLayers = function (meta) {
var res = [];
for (var i = 0; i < layers.length; ++i) {
if (layers[i] instanceof dwv.gui.ViewLayer) {
if (layers[i].getViewController().equalImageMeta(meta)) {
res.push(layers[i]);
}
}
}
return res;
};
/**
* Get the view layers data indices.
*
* @returns {Array} The list of indices.
*/
this.getViewDataIndices = function () {
var res = [];
for (var i = 0; i < layers.length; ++i) {
if (layers[i] instanceof dwv.gui.ViewLayer) {
res.push(layers[i].getDataIndex());
}
}
return res;
};
/**
* Get the active draw layer.
*
* @returns {object} The layer.
*/
this.getActiveDrawLayer = function () {
return layers[activeDrawLayerIndex];
};
/**
* Get the draw layers associated to a data index.
*
* @param {number} index The data index.
* @returns {Array} The layers.
*/
this.getDrawLayersByDataIndex = function (index) {
var res = [];
for (var i = 0; i < layers.length; ++i) {
if (layers[i] instanceof dwv.gui.DrawLayer &&
layers[i].getDataIndex() === index) {
res.push(layers[i]);
}
}
return res;
};
/**
* Set the active view layer.
*
* @param {number} index The index of the layer to set as active.
*/
this.setActiveViewLayer = function (index) {
activeViewLayerIndex = index;
};
/**
* Set the active view layer with a data index.
*
* @param {number} index The data index.
*/
this.setActiveViewLayerByDataIndex = function (index) {
for (var i = 0; i < layers.length; ++i) {
if (layers[i] instanceof dwv.gui.ViewLayer &&
layers[i].getDataIndex() === index) {
this.setActiveViewLayer(i);
break;
}
}
};
/**
* Set the active draw layer.
*
* @param {number} index The index of the layer to set as active.
*/
this.setActiveDrawLayer = function (index) {
activeDrawLayerIndex = index;
};
/**
* Set the active draw layer with a data index.
*
* @param {number} index The data index.
*/
this.setActiveDrawLayerByDataIndex = function (index) {
for (var i = 0; i < layers.length; ++i) {
if (layers[i] instanceof dwv.gui.DrawLayer &&
layers[i].getDataIndex() === index) {
this.setActiveDrawLayer(i);
break;
}
}
};
/**
* Add a view layer.
*
* @returns {object} The created layer.
*/
this.addViewLayer = function () {
// layer index
var viewLayerIndex = layers.length;
// create div
var div = getNextLayerDiv();
// prepend to container
containerDiv.append(div);
// view layer
var layer = new dwv.gui.ViewLayer(div);
// add layer
layers.push(layer);
// mark it as active
this.setActiveViewLayer(viewLayerIndex);
// bind view layer events
bindViewLayer(layer);
// return
return layer;
};
/**
* Add a draw layer.
*
* @returns {object} The created layer.
*/
this.addDrawLayer = function () {
// store active index
activeDrawLayerIndex = layers.length;
// create div
var div = getNextLayerDiv();
// prepend to container
containerDiv.append(div);
// draw layer
var layer = new dwv.gui.DrawLayer(div);
// add layer
layers.push(layer);
// bind draw layer events
bindDrawLayer(layer);
// return
return layer;
};
/**
* Bind view layer events to this.
*
* @param {object} viewLayer The view layer to bind.
*/
function bindViewLayer(viewLayer) {
// listen to position change to update other group layers
viewLayer.addEventListener(
'positionchange', self.updateLayersToPositionChange);
// propagate view viewLayer-layer events
for (var j = 0; j < dwv.image.viewEventNames.length; ++j) {
viewLayer.addEventListener(dwv.image.viewEventNames[j], fireEvent);
}
// propagate viewLayer events
viewLayer.addEventListener('renderstart', fireEvent);
viewLayer.addEventListener('renderend', fireEvent);
}
/**
* Bind draw layer events to this.
*
* @param {object} drawLayer The draw layer to bind.
*/
function bindDrawLayer(drawLayer) {
// propagate drawLayer events
drawLayer.addEventListener('drawcreate', fireEvent);
drawLayer.addEventListener('drawdelete', fireEvent);
}
/**
* Get the next layer DOM div.
*
* @returns {HTMLElement} A DOM div.
*/
function getNextLayerDiv() {
var div = document.createElement('div');
div.id = dwv.gui.getLayerDivId(self.getDivId(), layers.length);
div.className = 'layer';
div.style.pointerEvents = 'none';
return div;
}
/**
* Empty the layer list.
*/
this.empty = function () {
layers = [];
// reset active indices
activeViewLayerIndex = null;
activeDrawLayerIndex = null;
// clean container div
var previous = containerDiv.getElementsByClassName('layer');
if (previous) {
while (previous.length > 0) {
previous[0].remove();
}
}
};
/**
* Show a crosshair at a given position.
*
* @param {dwv.math.Point} position The position where to show the crosshair.
*/
function showCrosshairDiv(position) {
if (typeof position === 'undefined') {
position = currentPosition;
}
// remove previous
removeCrosshairDiv();
// use first layer as base for calculating position and
// line sizes
var layer0 = layers[0];
var vc = layer0.getViewController();
var p2D = vc.getPlanePositionFromPosition(position);
var displayPos = layer0.planePosToDisplay(p2D.x, p2D.y);
var lineH = document.createElement('hr');
lineH.id = self.getDivId() + '-scroll-crosshair-horizontal';
lineH.className = 'horizontal';
lineH.style.width = containerDiv.offsetWidth + 'px';
lineH.style.left = '0px';
lineH.style.top = displayPos.y + 'px';
var lineV = document.createElement('hr');
lineV.id = self.getDivId() + '-scroll-crosshair-vertical';
lineV.className = 'vertical';
lineV.style.width = containerDiv.offsetHeight + 'px';
lineV.style.left = (displayPos.x) + 'px';
lineV.style.top = '0px';
containerDiv.appendChild(lineH);
containerDiv.appendChild(lineV);
}
/**
* Remove crosshair divs.
*/
function removeCrosshairDiv() {
var div = document.getElementById(
self.getDivId() + '-scroll-crosshair-horizontal');
if (div) {
div.remove();
}
div = document.getElementById(
self.getDivId() + '-scroll-crosshair-vertical');
if (div) {
div.remove();
}
}
/**
* Update layers (but not the active view layer) to a position change.
*
* @param {object} event The position change event.
*/
this.updateLayersToPositionChange = function (event) {
// pause positionchange listeners
for (var j = 0; j < layers.length; ++j) {
if (layers[j] instanceof dwv.gui.ViewLayer) {
layers[j].removeEventListener(
'positionchange', self.updateLayersToPositionChange);
layers[j].removeEventListener('positionchange', fireEvent);
}
}
var index = new dwv.math.Index(event.value[0]);
var position = new dwv.math.Point(event.value[1]);
// store current position
currentPosition = position;
if (showCrosshair) {
showCrosshairDiv(position);
}
// origin of the first view layer
var baseViewLayerOrigin0 = null;
var baseViewLayerOrigin = null;
// update position for all layers except the source one
for (var i = 0; i < layers.length; ++i) {
// update base offset (does not trigger redraw)
// TODO check draw layers update
var hasSetOffset = false;
if (layers[i] instanceof dwv.gui.ViewLayer) {
var vc = layers[i].getViewController();
// origin0 should always be there
var origin0 = vc.getOrigin();
// depending on position, origin could be undefined
var origin = vc.getOrigin(position);
if (!baseViewLayerOrigin) {
baseViewLayerOrigin0 = origin0;
baseViewLayerOrigin = origin;
} else {
if (vc.canSetPosition(position) &&
typeof origin !== 'undefined') {
// TODO: compensate for possible different orientation between views
var scrollDiff = baseViewLayerOrigin0.minus(origin0);
var scrollOffset = new dwv.math.Vector3D(
scrollDiff.getX(), scrollDiff.getY(), scrollDiff.getZ());
var planeDiff = baseViewLayerOrigin.minus(origin);
var planeOffset = new dwv.math.Vector3D(
planeDiff.getX(), planeDiff.getY(), planeDiff.getZ());
hasSetOffset = layers[i].setBaseOffset(scrollOffset, planeOffset);
}
}
}
// update position (triggers redraw)
var hasSetPos = false;
if (layers[i].getId() !== event.srclayerid) {
hasSetPos = layers[i].setCurrentPosition(position, index);
}
// force redraw if needed
if (!hasSetPos && hasSetOffset) {
layers[i].draw();
}
}
// re-start positionchange listeners
for (var k = 0; k < layers.length; ++k) {
if (layers[k] instanceof dwv.gui.ViewLayer) {
layers[k].addEventListener(
'positionchange', self.updateLayersToPositionChange);
layers[k].addEventListener('positionchange', fireEvent);
}
}
};
/**
* Calculate the fit scale: the scale that fits the largest data.
*
* @returns {number|undefined} The fit scale.
*/
this.calculateFitScale = function () {
// check container
if (containerDiv.offsetWidth === 0 &&
containerDiv.offsetHeight === 0) {
throw new Error('Cannot fit to zero sized container.');
}
// get max size
var maxSize = this.getMaxSize();
if (typeof maxSize === 'undefined') {
return undefined;
}
// return best fit
return Math.min(
containerDiv.offsetWidth / maxSize.x,
containerDiv.offsetHeight / maxSize.y
);
};
/**
* Set the layer group fit scale.
*
* @param {number} scaleIn The fit scale.
*/
this.setFitScale = function (scaleIn) {
// get maximum size
var maxSize = this.getMaxSize();
// exit if none
if (typeof maxSize === 'undefined') {
return;
}
var containerSize = {
x: containerDiv.offsetWidth,
y: containerDiv.offsetHeight
};
// offset to keep data centered
var fitOffset = {
x: -0.5 * (containerSize.x - Math.floor(maxSize.x * scaleIn)),
y: -0.5 * (containerSize.y - Math.floor(maxSize.y * scaleIn))
};
// apply to layers
for (var j = 0; j < layers.length; ++j) {
layers[j].fitToContainer(scaleIn, containerSize, fitOffset);
}
// update crosshair
if (showCrosshair) {
showCrosshairDiv();
}
};
/**
* Get the largest data size.
*
* @returns {object|undefined} The largest size as {x,y}.
*/
this.getMaxSize = function () {
var maxSize = {x: 0, y: 0};
for (var j = 0; j < layers.length; ++j) {
if (layers[j] instanceof dwv.gui.ViewLayer) {
var size = layers[j].getImageWorldSize();
if (size.x > maxSize.x) {
maxSize.x = size.x;
}
if (size.y > maxSize.y) {
maxSize.y = size.y;
}
}
}
if (maxSize.x === 0 && maxSize.y === 0) {
maxSize = undefined;
}
return maxSize;
};
/**
* Flip all layers along the Z axis without offset compensation.
*/
this.flipScaleZ = function () {
baseScale.z *= -1;
this.setScale(baseScale);
};
/**
* Add scale to the layers. Scale cannot go lower than 0.1.
*
* @param {number} scaleStep The scale to add.
* @param {dwv.math.Point3D} center The scale center Point3D.
*/
this.addScale = function (scaleStep, center) {
var newScale = {
x: scale.x * (1 + scaleStep),
y: scale.y * (1 + scaleStep),
z: scale.z * (1 + scaleStep)
};
this.setScale(newScale, center);
};
/**
* Set the layers' scale.
*
* @param {object} newScale The scale to apply as {x,y,z}.
* @param {dwv.math.Point3D} center The scale center Point3D.
* @fires dwv.ctrl.LayerGroup#zoomchange
*/
this.setScale = function (newScale, center) {
scale = newScale;
// apply to layers
for (var i = 0; i < layers.length; ++i) {
layers[i].setScale(scale, center);
}
// event value
var value = [
newScale.x,
newScale.y,
newScale.z
];
if (typeof center !== 'undefined') {
value.push(center.getX());
value.push(center.getY());
value.push(center.getZ());
}
/**
* Zoom change event.
*
* @event dwv.ctrl.LayerGroup#zoomchange
* @type {object}
* @property {Array} value The changed value.
*/
fireEvent({
type: 'zoomchange',
value: value
});
};
/**
* Add translation to the layers.
*
* @param {object} translation The translation as {x,y,z}.
*/
this.addTranslation = function (translation) {
this.setOffset({
x: offset.x - translation.x,
y: offset.y - translation.y,
z: offset.z - translation.z
});
};
/**
* Set the layers' offset.
*
* @param {object} newOffset The offset as {x,y,z}.
* @fires dwv.ctrl.LayerGroup#offsetchange
*/
this.setOffset = function (newOffset) {
// store
offset = newOffset;
// apply to layers
for (var i = 0; i < layers.length; ++i) {
layers[i].setOffset(offset);
}
/**
* Offset change event.
*
* @event dwv.ctrl.LayerGroup#offsetchange
* @type {object}
* @property {Array} value The changed value.
*/
fireEvent({
type: 'offsetchange',
value: [offset.x, offset.y, offset.z],
});
};
/**
* Reset the stage to its initial scale and no offset.
*/
this.reset = function () {
this.setScale(baseScale);
this.setOffset({x: 0, y: 0, z: 0});
};
/**
* Draw the layer.
*/
this.draw = function () {
for (var i = 0; i < layers.length; ++i) {
layers[i].draw();
}
};
/**
* Display the layer.
*
* @param {boolean} flag Whether to display the layer or not.
*/
this.display = function (flag) {
for (var i = 0; i < layers.length; ++i) {
layers[i].display(flag);
}
};
/**
* 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) {
listenerHandler.fireEvent(event);
}
}; // LayerGroup class