/** @namespace */
var dwv = dwv || {};
/**
* Main application class.
*
* @class
* @example
* // create the dwv app
* var app = new dwv.App();
* // initialise
* app.init({
* dataViewConfigs: {'*': [{divId: 'layerGroup0'}]}
* });
* // load dicom data
* app.loadURLs([
* 'https://raw.githubusercontent.com/ivmartel/dwv/master/tests/data/bbmri-53323851.dcm'
* ]);
*/
dwv.App = function () {
// closure to self
var self = this;
// app options
var options = null;
// data controller
var dataController = null;
// toolbox controller
var toolboxController = null;
// load controller
var loadController = null;
// stage
var stage = null;
// UndoStack
var undoStack = null;
// Generic style
var style = new dwv.gui.Style();
/**
* Listener handler.
*
* @type {object}
* @private
*/
var listenerHandler = new dwv.utils.ListenerHandler();
/**
* Get the image.
*
* @param {number} index The data index.
* @returns {dwv.image.Image} The associated image.
*/
this.getImage = function (index) {
return dataController.get(index).image;
};
/**
* Get the last loaded image.
*
* @returns {dwv.image.Image} The image.
*/
this.getLastImage = function () {
return dataController.get(dataController.length() - 1).image;
};
/**
* Set the image at the given index.
*
* @param {number} index The data index.
* @param {dwv.image.Image} img The associated image.
*/
this.setImage = function (index, img) {
dataController.setImage(index, img);
};
/**
* Set the last image.
*
* @param {dwv.image.Image} img The associated image.
*/
this.setLastImage = function (img) {
dataController.setImage(dataController.length() - 1, img);
};
/**
* Add a new image.
*
* @param {dwv.image.Image} image The new image.
* @param {object} meta The image meta.
* @returns {number} The new image id.
*/
this.addNewImage = function (image, meta) {
var id = dataController.length();
// load start event
fireEvent({
type: 'loadstart',
loadtype: 'image',
source: 'internal',
loadid: id
});
// add image to data controller
dataController.addNew(id, image, meta);
// load item event
fireEvent({
type: 'loaditem',
loadtype: 'image',
data: meta,
source: 'internal',
loadid: id,
isfirstitem: true
});
// optional render
if (options.viewOnFirstLoadItem) {
this.render(id);
}
// load events
fireEvent({
type: 'load',
loadtype: 'image',
source: 'internal',
loadid: id
});
fireEvent({
type: 'loadend',
loadtype: 'image',
source: 'internal',
loadid: id
});
return id;
};
/**
* Get the meta data.
*
* @param {number} index The data index.
* @returns {object} The list of meta data.
*/
this.getMetaData = function (index) {
return dataController.get(index).meta;
};
/**
* Get the number of loaded data.
*
* @returns {number} The number.
*/
this.getNumberOfLoadedData = function () {
return dataController.length();
};
/**
* Can the data be scrolled?
*
* @returns {boolean} True if the data has a third dimension greater than one.
*/
this.canScroll = function () {
var viewLayer = stage.getActiveLayerGroup().getActiveViewLayer();
var controller = viewLayer.getViewController();
return controller.canScroll();
};
/**
* Can window and level be applied to the data?
*
* @returns {boolean} True if the data is monochrome.
*/
this.canWindowLevel = function () {
var viewLayer = stage.getActiveLayerGroup().getActiveViewLayer();
var controller = viewLayer.getViewController();
return controller.canWindowLevel();
};
/**
* Get the layer scale on top of the base scale.
*
* @returns {object} The scale as {x,y}.
*/
this.getAddedScale = function () {
return stage.getActiveLayerGroup().getAddedScale();
};
/**
* Get the base scale.
*
* @returns {object} The scale as {x,y}.
*/
this.getBaseScale = function () {
return stage.getActiveLayerGroup().getBaseScale();
};
/**
* Get the layer offset.
*
* @returns {object} The offset.
*/
this.getOffset = function () {
return stage.getActiveLayerGroup().getOffset();
};
/**
* Get the toolbox controller.
*
* @returns {object} The controller.
*/
this.getToolboxController = function () {
return toolboxController;
};
/**
* Get the active layer group.
* The layer is available after the first loaded item.
*
* @returns {dwv.gui.LayerGroup} The layer group.
*/
this.getActiveLayerGroup = function () {
return stage.getActiveLayerGroup();
};
/**
* Get the view layers associated to a data index.
* The layer are available after the first loaded item.
*
* @param {number} index The data index.
* @returns {Array} The layers.
*/
this.getViewLayersByDataIndex = function (index) {
return stage.getViewLayersByDataIndex(index);
};
/**
* Get the draw layers associated to a data index.
* The layer are available after the first loaded item.
*
* @param {number} index The data index.
* @returns {Array} The layers.
*/
this.getDrawLayersByDataIndex = function (index) {
return stage.getDrawLayersByDataIndex(index);
};
/**
* Get a layer group by div id.
* The layer is available after the first loaded item.
*
* @param {string} divId The div id.
* @returns {dwv.gui.LayerGroup} The layer group.
*/
this.getLayerGroupByDivId = function (divId) {
return stage.getLayerGroupByDivId(divId);
};
/**
* Get the number of layer groups.
*
* @returns {number} The number of groups.
*/
this.getNumberOfLayerGroups = function () {
return stage.getNumberOfLayerGroups();
};
/**
* Get the app style.
*
* @returns {object} The app style.
*/
this.getStyle = function () {
return style;
};
/**
* Add a command to the undo stack.
*
* @param {object} cmd The command to add.
* @fires dwv.tool.UndoStack#undoadd
*/
this.addToUndoStack = function (cmd) {
if (undoStack !== null) {
undoStack.add(cmd);
}
};
/**
* Initialise the application.
*
* @param {object} opt The application option with:
* - `dataViewConfigs`: data indexed object containing the data view
* configurations in the form of a list of objects containing:
* - divId: the HTML div id
* - orientation: optional 'axial', 'coronal' or 'sagittal' orientation
* string (default undefined keeps the original slice order)
* - `binders`: array of layerGroup binders
* - `tools`: tool name indexed object containing individual tool
* configurations in the form of a list of objects containing:
* - options: array of tool options
* - `viewOnFirstLoadItem`: boolean flag to trigger the first data render
* after the first loaded data or not
* - `defaultCharacterSet`: the default chraracter set string used for DICOM
* parsing
* @example
* // create the dwv app
* var app = new dwv.App();
* // initialise
* app.init({
* dataViewConfigs: {'*': [{divId: 'layerGroup0'}]},
* viewOnFirstLoadItem: false
* });
* // render button
* var button = document.createElement('button');
* button.id = 'render';
* button.disabled = true;
* button.appendChild(document.createTextNode('render'));
* document.body.appendChild(button);
* app.addEventListener('load', function () {
* var button = document.getElementById('render');
* button.disabled = false;
* button.onclick = function () {
* // render data #0
* app.render(0);
* };
* });
* // load dicom data
* app.loadURLs([
* 'https://raw.githubusercontent.com/ivmartel/dwv/master/tests/data/bbmri-53323851.dcm'
* ]);
*/
this.init = function (opt) {
// store
options = opt;
// defaults
if (typeof options.viewOnFirstLoadItem === 'undefined') {
options.viewOnFirstLoadItem = true;
}
// undo stack
undoStack = new dwv.tool.UndoStack();
undoStack.addEventListener('undoadd', fireEvent);
undoStack.addEventListener('undo', fireEvent);
undoStack.addEventListener('redo', fireEvent);
// tools
if (options.tools && options.tools.length !== 0) {
// setup the tool list
var toolList = {};
var keys = Object.keys(options.tools);
for (var t = 0; t < keys.length; ++t) {
var toolName = keys[t];
var toolParams = options.tools[toolName];
// find the tool in the dwv.tool namespace
if (typeof dwv.tool[toolName] !== 'undefined') {
// create tool instance
toolList[toolName] = new dwv.tool[toolName](this);
// register listeners
if (typeof toolList[toolName].addEventListener !== 'undefined') {
var names = toolList[toolName].getEventNames();
for (var j = 0; j < names.length; ++j) {
toolList[toolName].addEventListener(names[j], fireEvent);
}
}
// tool options
if (typeof toolParams.options !== 'undefined') {
var type = 'raw';
if (typeof toolList[toolName].getOptionsType !== 'undefined') {
type = toolList[toolName].getOptionsType();
}
var toolOptions = toolParams.options;
if (type === 'instance' ||
type === 'factory') {
toolOptions = {};
for (var i = 0; i < toolParams.options.length; ++i) {
var optionName = toolParams.options[i];
var optionClassName = optionName;
if (type === 'factory') {
optionClassName += 'Factory';
}
var toolNamespace = toolName.charAt(0).toLowerCase() +
toolName.slice(1);
if (typeof dwv.tool[toolNamespace][optionClassName] !==
'undefined') {
toolOptions[optionName] =
dwv.tool[toolNamespace][optionClassName];
} else {
dwv.logger.warn('Could not find option class for: ' +
optionName);
}
}
}
toolList[toolName].setOptions(toolOptions);
}
} else {
dwv.logger.warn('Could not initialise unknown tool: ' + toolName);
}
}
// add tools to the controller
toolboxController = new dwv.ctrl.ToolboxController(toolList);
}
// create load controller
loadController = new dwv.ctrl.LoadController(options.defaultCharacterSet);
loadController.onloadstart = onloadstart;
loadController.onprogress = onloadprogress;
loadController.onloaditem = onloaditem;
loadController.onload = onload;
loadController.onloadend = onloadend;
loadController.onerror = onloaderror;
loadController.onabort = onloadabort;
// create data controller
dataController = new dwv.ctrl.DataController();
// create stage
stage = new dwv.gui.Stage();
if (typeof options.binders !== 'undefined') {
stage.setBinders(options.binders);
}
};
/**
* Get a HTML element associated to the application.
*
* @param {string} _name The name or id to find.
* @returns {object} The found element or null.
* @deprecated
*/
this.getElement = function (_name) {
return null;
};
/**
* Reset the application.
*/
this.reset = function () {
// clear objects
dataController.reset();
stage.empty();
// reset undo/redo
if (undoStack) {
undoStack = new dwv.tool.UndoStack();
undoStack.addEventListener('undoadd', fireEvent);
undoStack.addEventListener('undo', fireEvent);
undoStack.addEventListener('redo', fireEvent);
}
};
/**
* Reset the layout of the application.
*/
this.resetLayout = function () {
stage.reset();
stage.draw();
};
/**
* 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);
};
// load API [begin] -------------------------------------------------------
/**
* Load a list of files. Can be image files or a state file.
*
* @param {Array} files The list of files to load.
* @fires dwv.App#loadstart
* @fires dwv.App#loadprogress
* @fires dwv.App#loaditem
* @fires dwv.App#loadend
* @fires dwv.App#error
* @fires dwv.App#abort
*/
this.loadFiles = function (files) {
if (files.length === 0) {
dwv.logger.warn('Ignoring empty input file list.');
return;
}
loadController.loadFiles(files);
};
/**
* Load a list of URLs. Can be image files or a state file.
*
* @param {Array} urls The list of urls to load.
* @param {object} options The options object, can contain:
* - requestHeaders: an array of {name, value} to use as request headers
* - withCredentials: boolean xhr.withCredentials flag to pass to the request
* - batchSize: the size of the request url batch
* @fires dwv.App#loadstart
* @fires dwv.App#loadprogress
* @fires dwv.App#loaditem
* @fires dwv.App#loadend
* @fires dwv.App#error
* @fires dwv.App#abort
*/
this.loadURLs = function (urls, options) {
if (urls.length === 0) {
dwv.logger.warn('Ignoring empty input url list.');
return;
}
loadController.loadURLs(urls, options);
};
/**
* Load a list of ArrayBuffers.
*
* @param {Array} data The list of ArrayBuffers to load
* in the form of [{name: "", filename: "", data: data}].
* @fires dwv.App#loadstart
* @fires dwv.App#loadprogress
* @fires dwv.App#loaditem
* @fires dwv.App#loadend
* @fires dwv.App#error
* @fires dwv.App#abort
*/
this.loadImageObject = function (data) {
loadController.loadImageObject(data);
};
/**
* Abort the current load.
*/
this.abortLoad = function () {
loadController.abort();
};
// load API [end] ---------------------------------------------------------
/**
* Fit the display to the data of each layer group.
* To be called once the image is loaded.
*/
this.fitToContainer = function () {
stage.syncLayerGroupScale();
};
/**
* Init the Window/Level display
*/
this.initWLDisplay = function () {
var viewLayer = stage.getActiveLayerGroup().getActiveViewLayer();
var controller = viewLayer.getViewController();
controller.initialise();
};
/**
* Get the layer group configuration from a data index.
* Defaults to div id 'layerGroup' if no association object has been set.
*
* @param {number} dataIndex The data index.
* @returns {Array} The list of associated configs.
*/
function getViewConfigs(dataIndex) {
// check options
if (options.dataViewConfigs === null ||
typeof options.dataViewConfigs === 'undefined') {
throw new Error('No available data view configuration');
}
var configs = [];
if (typeof options.dataViewConfigs['*'] !== 'undefined') {
configs = options.dataViewConfigs['*'];
} else if (typeof options.dataViewConfigs[dataIndex] !== 'undefined') {
configs = options.dataViewConfigs[dataIndex];
}
return configs;
}
/**
* Get the data view config.
* Carefull, returns a reference, do not modify without resetting.
*
* @returns {object} The configuration list.
*/
this.getDataViewConfig = function () {
return options.dataViewConfigs;
};
/**
* Set the data view configuration (see the init options for details).
*
* @param {object} configs The configuration list.
*/
this.setDataViewConfig = function (configs) {
// clean up
stage.empty();
// set new
options.dataViewConfigs = configs;
// create layer groups
createLayerGroups(configs);
};
/**
* Create layer groups according to a data view config:
* adds them to stage and bind them.
*
* @param {object} dataViewConfigs The data view config.
*/
function createLayerGroups(dataViewConfigs) {
var dataKeys = Object.keys(dataViewConfigs);
var divIds = [];
for (var i = 0; i < dataKeys.length; ++i) {
var dataConfigs = dataViewConfigs[dataKeys[i]];
for (var j = 0; j < dataConfigs.length; ++j) {
var viewConfig = dataConfigs[j];
// view configs can contain the same divIds, avoid duplicating
if (!divIds.includes(viewConfig.divId)) {
// create new layer group
var element = document.getElementById(viewConfig.divId);
var layerGroup = stage.addLayerGroup(element);
// bind events
bindLayerGroupToApp(layerGroup);
// optional orientation
if (typeof viewConfig.orientation !== 'undefined') {
layerGroup.setTargetOrientation(
dwv.math.getMatrixFromName(viewConfig.orientation));
}
divIds.push(viewConfig.divId);
}
}
}
}
/**
* Set the layer groups binders.
*
* @param {Array} list The binders list.
*/
this.setLayerGroupsBinders = function (list) {
stage.setBinders(list);
};
/**
* Render the current data.
*
* @param {number} dataIndex The data index to render.
*/
this.render = function (dataIndex) {
if (typeof dataIndex === 'undefined' || dataIndex === null) {
throw new Error('Cannot render without data index');
}
// create layer groups if not done yet
// (create all to allow for ratio sync)
if (stage.getNumberOfLayerGroups() === 0) {
createLayerGroups(options.dataViewConfigs);
}
// loop on all configs
var viewConfigs = getViewConfigs(dataIndex);
// nothing to do if no view config
if (viewConfigs.length === 0) {
dwv.logger.info('Not rendering data: ' + dataIndex +
' (no data view config)');
return;
}
for (var i = 0; i < viewConfigs.length; ++i) {
var config = viewConfigs[i];
var layerGroup =
stage.getLayerGroupByDivId(config.divId);
// layer group must exist
if (!layerGroup) {
throw new Error('No layer group for ' + config.divId);
}
// initialise or add view
// warn: needs a loaded DOM
if (layerGroup.getViewLayersByDataIndex(dataIndex).length === 0) {
if (layerGroup.getNumberOfLayers() === 0) {
initialiseBaseLayers(dataIndex, config);
} else {
addViewLayer(dataIndex, config);
}
}
// draw
layerGroup.draw();
}
};
/**
* Zoom to the layers.
*
* @param {number} step The step to add to the current zoom.
* @param {number} cx The zoom center X coordinate.
* @param {number} cy The zoom center Y coordinate.
*/
this.zoom = function (step, cx, cy) {
var layerGroup = stage.getActiveLayerGroup();
var viewController = layerGroup.getActiveViewLayer().getViewController();
var k = viewController.getCurrentScrollPosition();
var center = new dwv.math.Point3D(cx, cy, k);
layerGroup.addScale(step, center);
layerGroup.draw();
};
/**
* Apply a translation to the layers.
*
* @param {number} tx The translation along X.
* @param {number} ty The translation along Y.
*/
this.translate = function (tx, ty) {
var layerGroup = stage.getActiveLayerGroup();
layerGroup.addTranslation({x: tx, y: ty});
layerGroup.draw();
};
/**
* Set the image layer opacity.
*
* @param {number} alpha The opacity ([0:1] range).
*/
this.setOpacity = function (alpha) {
var viewLayer = stage.getActiveLayerGroup().getActiveViewLayer();
viewLayer.setOpacity(alpha);
viewLayer.draw();
};
/**
* Set the drawings on the current stage.
*
* @param {Array} drawings An array of drawings.
* @param {Array} drawingsDetails An array of drawings details.
*/
this.setDrawings = function (drawings, drawingsDetails) {
var layerGroup = stage.getActiveLayerGroup();
var viewController =
layerGroup.getActiveViewLayer().getViewController();
var drawController =
layerGroup.getActiveDrawLayer().getDrawController();
drawController.setDrawings(
drawings, drawingsDetails, fireEvent, this.addToUndoStack);
drawController.activateDrawLayer(
viewController.getCurrentOrientedIndex(),
viewController.getScrollIndex());
};
/**
* Get the JSON state of the app.
*
* @returns {object} The state of the app as a JSON object.
*/
this.getState = function () {
var state = new dwv.io.State();
return state.toJSON(self);
};
// Handler Methods -----------------------------------------------------------
/**
* Handle resize: fit the display to the window.
* To be called once the image is loaded.
* Can be connected to a window 'resize' event.
*
* @param {object} _event The change event.
* @private
*/
this.onResize = function (_event) {
self.fitToContainer();
};
/**
* Key down callback. Meant to be used in tools.
*
* @param {object} event The key down event.
* @fires dwv.App#keydown
*/
this.onKeydown = function (event) {
/**
* Key down event.
*
* @event dwv.App#keydown
* @type {KeyboardEvent}
* @property {string} type The event type: keydown.
* @property {string} context The tool where the event originated.
*/
fireEvent(event);
};
/**
* Key down event handler example.
* - CRTL-Z: undo
* - CRTL-Y: redo
* - CRTL-ARROW_LEFT: next element on fourth dim
* - CRTL-ARROW_UP: next element on third dim
* - CRTL-ARROW_RIGHT: previous element on fourth dim
* - CRTL-ARROW_DOWN: previous element on third dim
*
* @param {object} event The key down event.
* @fires dwv.tool.UndoStack#undo
* @fires dwv.tool.UndoStack#redo
*/
this.defaultOnKeydown = function (event) {
if (event.ctrlKey) {
if (event.shiftKey) {
var viewController =
stage.getActiveLayerGroup().getActiveViewLayer().getViewController();
var size = viewController.getImageSize();
if (event.key === 'ArrowLeft') { // crtl-shift-arrow-left
if (size.moreThanOne(3)) {
viewController.decrementIndex(3);
}
} else if (event.key === 'ArrowUp') { // crtl-shift-arrow-up
if (viewController.canScroll()) {
viewController.incrementScrollIndex();
}
} else if (event.key === 'ArrowRight') { // crtl-shift-arrow-right
if (size.moreThanOne(3)) {
viewController.incrementIndex(3);
}
} else if (event.key === 'ArrowDown') { // crtl-shift-arrow-down
if (viewController.canScroll()) {
viewController.decrementScrollIndex();
}
}
} else if (event.key === 'y') { // crtl-y
undoStack.redo();
} else if (event.key === 'z') { // crtl-z
undoStack.undo();
} else if (event.key === ' ') { // crtl-space
for (var i = 0; i < stage.getNumberOfLayerGroups(); ++i) {
stage.getLayerGroup(i).setShowCrosshair(
!stage.getLayerGroup(i).getShowCrosshair()
);
}
}
}
};
// Internal members shortcuts-----------------------------------------------
/**
* Reset the display
*/
this.resetDisplay = function () {
self.resetLayout();
self.initWLDisplay();
};
/**
* Reset the app zoom.s
*/
this.resetZoom = function () {
self.resetLayout();
};
/**
* Set the colour map.
*
* @param {string} colourMap The colour map name.
*/
this.setColourMap = function (colourMap) {
var viewController =
stage.getActiveLayerGroup().getActiveViewLayer().getViewController();
viewController.setColourMapFromName(colourMap);
};
/**
* Set the window/level preset.
*
* @param {object} preset The window/level preset.
*/
this.setWindowLevelPreset = function (preset) {
var viewController =
stage.getActiveLayerGroup().getActiveViewLayer().getViewController();
viewController.setWindowLevelPreset(preset);
};
/**
* Set the tool
*
* @param {string} tool The tool.
*/
this.setTool = function (tool) {
// bind tool to active layer
for (var i = 0; i < stage.getNumberOfLayerGroups(); ++i) {
var layerGroup = stage.getLayerGroup(i);
// draw or view layer
var layer = null;
if (tool === 'Draw' ||
tool === 'Livewire' ||
tool === 'Floodfill') {
layer = layerGroup.getActiveDrawLayer();
} else {
layer = layerGroup.getActiveViewLayer();
}
if (layer) {
toolboxController.bindLayer(layer, layerGroup.getDivId());
}
}
// set toolbox tool
toolboxController.setSelectedTool(tool);
};
/**
* Set the tool live features.
*
* @param {object} list The list of features.
*/
this.setToolFeatures = function (list) {
toolboxController.setToolFeatures(list);
};
/**
* Undo the last action
*
* @fires dwv.tool.UndoStack#undo
*/
this.undo = function () {
undoStack.undo();
};
/**
* Redo the last action
*
* @fires dwv.tool.UndoStack#redo
*/
this.redo = function () {
undoStack.redo();
};
/**
* Get the undo stack size.
*
* @returns {number} The size of the stack.
*/
this.getStackSize = function () {
return undoStack.getStackSize();
};
/**
* Get the current undo stack index.
*
* @returns {number} The stack index.
*/
this.getCurrentStackIndex = function () {
return undoStack.getCurrentStackIndex();
};
// Private Methods -----------------------------------------------------------
/**
* 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);
}
/**
* Data load start callback.
*
* @param {object} event The load start event.
* @private
*/
function onloadstart(event) {
/**
* Load start event.
*
* @event dwv.App#loadstart
* @type {object}
* @property {string} type The event type: loadstart.
* @property {string} loadType The load type: image or state.
* @property {*} source The load source: string for an url,
* File for a file.
*/
event.type = 'loadstart';
fireEvent(event);
}
/**
* Data load progress callback.
*
* @param {object} event The progress event.
* @private
*/
function onloadprogress(event) {
/**
* Load progress event.
*
* @event dwv.App#loadprogress
* @type {object}
* @property {string} type The event type: loadprogress.
* @property {string} loadType The load type: image or state.
* @property {*} source The load source: string for an url,
* File for a file.
* @property {number} loaded The loaded percentage.
* @property {number} total The total percentage.
*/
event.type = 'loadprogress';
fireEvent(event);
}
/**
* Data load callback.
*
* @param {object} event The load event.
* @private
*/
function onloaditem(event) {
// check event
if (typeof event.data === 'undefined') {
dwv.logger.error('Missing loaditem event data.');
}
if (typeof event.loadtype === 'undefined') {
dwv.logger.error('Missing loaditem event load type.');
}
var isFirstLoadItem = event.isfirstitem;
var eventMetaData = null;
if (event.loadtype === 'image') {
if (isFirstLoadItem) {
dataController.addNew(
event.loadid, event.data.image, event.data.info);
} else {
dataController.update(
event.loadid, event.data.image, event.data.info);
}
eventMetaData = event.data.info;
} else if (event.loadtype === 'state') {
var state = new dwv.io.State();
state.apply(self, state.fromJSON(event.data));
eventMetaData = 'state';
}
/**
* Load item event: fired when a load item is successfull.
*
* @event dwv.App#loaditem
* @type {object}
* @property {string} type The event type: loaditem.
* @property {string} loadType The load type: image or state.
* @property {*} source The load source: string for an url,
* File for a file.
* @property {object} data The loaded meta data.
*/
fireEvent({
type: 'loaditem',
data: eventMetaData,
source: event.source,
loadtype: event.loadtype,
loadid: event.loadid,
isfirstitem: event.isfirstitem
});
// render if first and flag allows
if (event.loadtype === 'image' &&
getViewConfigs(event.loadid).length !== 0 &&
isFirstLoadItem && options.viewOnFirstLoadItem) {
self.render(event.loadid);
}
}
/**
* Data load callback.
*
* @param {object} event The load event.
* @private
*/
function onload(event) {
/**
* Load event: fired when a load finishes successfully.
*
* @event dwv.App#load
* @type {object}
* @property {string} type The event type: load.
* @property {string} loadType The load type: image or state.
*/
event.type = 'load';
fireEvent(event);
}
/**
* Data load end callback.
*
* @param {object} event The load end event.
* @private
*/
function onloadend(event) {
/**
* Main load end event: fired when the load finishes,
* successfully or not.
*
* @event dwv.App#loadend
* @type {object}
* @property {string} type The event type: loadend.
* @property {string} loadType The load type: image or state.
* @property {*} source The load source: string for an url,
* File for a file.
*/
event.type = 'loadend';
fireEvent(event);
}
/**
* Data load error callback.
*
* @param {object} event The error event.
* @private
*/
function onloaderror(event) {
/**
* Load error event.
*
* @event dwv.App#loaderror
* @type {object}
* @property {string} type The event type: error.
* @property {string} loadType The load type: image or state.
* @property {*} source The load source: string for an url,
* File for a file.
* @property {object} error The error.
* @property {object} target The event target.
*/
event.type = 'loaderror';
fireEvent(event);
}
/**
* Data load abort callback.
*
* @param {object} event The abort event.
* @private
*/
function onloadabort(event) {
/**
* Load abort event.
*
* @event dwv.App#loadabort
* @type {object}
* @property {string} type The event type: abort.
* @property {string} loadType The load type: image or state.
* @property {*} source The load source: string for an url,
* File for a file.
*/
event.type = 'loadabort';
fireEvent(event);
}
/**
* Bind layer group events to app.
*
* @param {object} group The layer group.
* @private
*/
function bindLayerGroupToApp(group) {
// propagate layer group events
group.addEventListener('zoomchange', fireEvent);
group.addEventListener('offsetchange', fireEvent);
// propagate viewLayer events
group.addEventListener('renderstart', fireEvent);
group.addEventListener('renderend', fireEvent);
// propagate view events
for (var j = 0; j < dwv.image.viewEventNames.length; ++j) {
group.addEventListener(dwv.image.viewEventNames[j], fireEvent);
}
// propagate drawLayer events
if (toolboxController && toolboxController.hasTool('Draw')) {
group.addEventListener('drawcreate', fireEvent);
group.addEventListener('drawdelete', fireEvent);
}
}
/**
* Initialise the layers.
* To be called once the DICOM data has been loaded.
*
* @param {number} dataIndex The data index.
* @param {object} dataViewConfig The data view config.
* @private
*/
function initialiseBaseLayers(dataIndex, dataViewConfig) {
// add layers
addViewLayer(dataIndex, dataViewConfig);
// initialise the toolbox
if (toolboxController) {
toolboxController.init();
}
}
/**
* Add a view layer.
*
* @param {number} dataIndex The data index.
* @param {object} dataViewConfig The data view config.
*/
function addViewLayer(dataIndex, dataViewConfig) {
var data = dataController.get(dataIndex);
if (!data) {
throw new Error('Cannot initialise layer with data id: ' + dataIndex);
}
var layerGroup = stage.getLayerGroupByDivId(dataViewConfig.divId);
if (!layerGroup) {
throw new Error('Cannot initialise layer with group id: ' +
dataViewConfig.divId);
}
var imageGeometry = data.image.getGeometry();
// un-bind
stage.unbindLayerGroups();
// create and setup view
var viewFactory = new dwv.ViewFactory();
var view = viewFactory.create(data.meta, data.image);
var viewOrientation = dwv.gui.getViewOrientation(
imageGeometry.getOrientation(),
layerGroup.getTargetOrientation()
);
view.setOrientation(viewOrientation);
// make pixel of value 0 transparent for segmentation
// (assuming RGB data)
if (data.image.getMeta().Modality === 'SEG') {
view.setAlphaFunction(function (value /*, index*/) {
if (value[0] === 0 &&
value[1] === 0 &&
value[2] === 0) {
return 0;
} else {
return 0xff;
}
});
}
// colour map
if (typeof dataViewConfig.colourMap !== 'undefined') {
view.setColourMap(dataViewConfig.colourMap);
}
var isBaseLayer = layerGroup.getNumberOfLayers() === 0;
// opacity
var opacity = 1;
// do we have more than one layer
// (the layer has not been added to the layer group yet)
if (!isBaseLayer) {
opacity = 0.5;
// set color map if non was provided
if (typeof dataViewConfig.colourMap === 'undefined') {
view.setColourMap(dwv.image.lut.rainbow);
}
}
// view layer
var viewLayer = layerGroup.addViewLayer();
viewLayer.setView(view, dataIndex);
var size2D = imageGeometry.getSize(viewOrientation).get2D();
var spacing2D = imageGeometry.getSpacing(viewOrientation).get2D();
viewLayer.initialise(size2D, spacing2D, opacity);
var viewController = viewLayer.getViewController();
// listen to controller events
if (data.image.getMeta().Modality === 'SEG') {
viewController.addEventListener('masksegmentdelete', fireEvent);
viewController.addEventListener('masksegmentredraw', fireEvent);
}
// listen to image changes
dataController.addEventListener('imageset', viewLayer.onimageset);
dataController.addEventListener('imagechange', function (event) {
viewLayer.onimagechange(event);
self.render(event.dataid);
});
// bind
stage.bindLayerGroups();
if (toolboxController) {
toolboxController.bindLayer(viewLayer, layerGroup.getDivId());
}
// optional draw layer
var drawLayer;
if (toolboxController && toolboxController.hasTool('Draw')) {
drawLayer = layerGroup.addDrawLayer();
drawLayer.initialise(size2D, spacing2D, dataIndex);
drawLayer.setPlaneHelper(viewLayer.getViewController().getPlaneHelper());
}
// sync layers position
var value = [
viewController.getCurrentIndex().getValues(),
viewController.getCurrentPosition().getValues()
];
layerGroup.updateLayersToPositionChange({
value: value,
srclayerid: viewLayer.getId()
});
// sync layer groups
stage.syncLayerGroupScale();
// major orientation axis
var major = imageGeometry.getOrientation().getThirdColMajorDirection();
// view layer offset (done before scale)
viewLayer.setOffset(layerGroup.getOffset());
// extra flip offset for oriented views...
if (typeof dataViewConfig.orientation !== 'undefined') {
if (major === 2) {
// flip offset Y for axial aquired data
if (dataViewConfig.orientation !== 'axial') {
viewLayer.addFlipOffsetY();
if (typeof drawLayer !== 'undefined') {
drawLayer.addFlipOffsetY();
}
}
} else if (major === 0) {
// flip offset X for sagittal aquired data
if (dataViewConfig.orientation !== 'sagittal') {
viewLayer.addFlipOffsetX();
if (typeof drawLayer !== 'undefined') {
drawLayer.addFlipOffsetX();
}
}
}
}
// view layer scale
// only flip scale for base layers
if (isBaseLayer) {
if (typeof dataViewConfig.orientation !== 'undefined') {
if (major === 0 || major === 2) {
// scale flip Z for oriented views...
layerGroup.flipScaleZ();
} else {
viewLayer.setScale(layerGroup.getScale());
if (typeof drawLayer !== 'undefined') {
drawLayer.setScale(layerGroup.getScale());
}
}
} else {
if (major === 0) {
// scale flip Z for sagittal and undefined target orientation
layerGroup.flipScaleZ();
} else {
viewLayer.setScale(layerGroup.getScale());
if (typeof drawLayer !== 'undefined') {
drawLayer.setScale(layerGroup.getScale());
}
}
}
} else {
viewLayer.setScale(layerGroup.getScale());
if (typeof drawLayer !== 'undefined') {
drawLayer.setScale(layerGroup.getScale());
}
}
}
}; // class dwv.App