// namespaces
var dwv = dwv || {};
dwv.image = dwv.image || {};
/**
* List of view event names.
*
* @type {Array}
*/
dwv.image.viewEventNames = [
'wlchange',
'wlpresetadd',
'colourchange',
'positionchange',
'opacitychange',
'alphafuncchange'
];
/**
* View class.
*
* @class
* @param {Image} image The associated image.
* Need to set the window lookup table once created
* (either directly or with helper methods).
*/
dwv.image.View = function (image) {
// closure to self
var self = this;
// listen to appendframe event to update the current position
// to add the extra dimension
image.addEventListener('appendframe', function () {
// update current position if first appendFrame
var position = self.getCurrentPosition();
if (position.length() === 3) {
// add dimension
var values = position.getValues();
values.push(0);
self.setCurrentPosition(new dwv.math.Point(values));
}
});
/**
* Window lookup tables, indexed per Rescale Slope and Intercept (RSI).
*
* @private
* @type {Window}
*/
var windowLuts = {};
/**
* Window presets.
* Minmax will be filled at first use (see view.setWindowLevelPreset).
*
* @private
* @type {object}
*/
var windowPresets = {minmax: {name: 'minmax'}};
/**
* Current window preset name.
*
* @private
* @type {string}
*/
var currentPresetName = null;
/**
* Current window level.
*
* @private
* @type {object}
*/
var currentWl = null;
/**
* colour map.
*
* @private
* @type {object}
*/
var colourMap = dwv.image.lut.plain;
/**
* Current position as a Point3D.
*
* @private
* @type {object}
*/
var currentPosition = null;
/**
* View orientation. Undefined will use the original slice ordering.
*
* @private
* @type {object}
*/
var orientation;
/**
* Listener handler.
*
* @type {object}
* @private
*/
var listenerHandler = new dwv.utils.ListenerHandler();
/**
* Get the associated image.
*
* @returns {Image} The associated image.
*/
this.getImage = function () {
return image;
};
/**
* Set the associated image.
*
* @param {Image} inImage The associated image.
*/
this.setImage = function (inImage) {
image = inImage;
};
/**
* Get the view orientation.
*
* @returns {dwv.math.Matrix33} The orientation matrix.
*/
this.getOrientation = function () {
return orientation;
};
/**
* Set the view orientation.
*
* @param {dwv.math.Matrix33} mat33 The orientation matrix.
*/
this.setOrientation = function (mat33) {
orientation = mat33;
};
/**
* Set initial position.
*/
this.setInitialPosition = function () {
var silent = true;
var geometry = image.getGeometry();
var values = new Array(geometry.getSize().length());
values.fill(0);
var index = new dwv.math.Index(values);
this.setCurrentPosition(
geometry.indexToWorld(index),
silent
);
};
/**
* Get the milliseconds per frame from frame rate.
*
* @param {number} recommendedDisplayFrameRate Recommended Display Frame Rate.
* @returns {number} The milliseconds per frame.
*/
this.getPlaybackMilliseconds = function (recommendedDisplayFrameRate) {
if (!recommendedDisplayFrameRate) {
// Default to 10 FPS if none is found in the meta
recommendedDisplayFrameRate = 10;
}
// round milliseconds per frame to nearest whole number
return Math.round(1000 / recommendedDisplayFrameRate);
};
/**
* Per value alpha function.
*
* @param {*} _value The pixel value. Can be a number for monochrome
* data or an array for RGB data.
* @returns {number} The coresponding alpha [0,255].
*/
var alphaFunction = function (_value) {
// default always returns fully visible
return 0xff;
};
/**
* Get the alpha function.
*
* @returns {Function} The function.
*/
this.getAlphaFunction = function () {
return alphaFunction;
};
/**
* Set alpha function.
*
* @param {Function} func The function.
* @fires dwv.image.View#alphafuncchange
*/
this.setAlphaFunction = function (func) {
alphaFunction = func;
/**
* Alpha func change event.
*
* @event dwv.image.View#alphafuncchange
* @type {object}
*/
fireEvent({
type: 'alphafuncchange'
});
};
/**
* Get the window LUT of the image.
* Warning: can be undefined in no window/level was set.
*
* @param {object} rsi Optional image rsi, will take the one of the
* current slice otherwise.
* @returns {Window} The window LUT of the image.
* @fires dwv.image.View#wlchange
*/
this.getCurrentWindowLut = function (rsi) {
// check position
if (!this.getCurrentPosition()) {
this.setInitialPosition();
}
var currentIndex = this.getCurrentIndex();
// use current rsi if not provided
if (typeof rsi === 'undefined') {
rsi = image.getRescaleSlopeAndIntercept(currentIndex);
}
// get the current window level
var wl = null;
// special case for 'perslice' presets
if (currentPresetName &&
typeof windowPresets[currentPresetName] !== 'undefined' &&
typeof windowPresets[currentPresetName].perslice !== 'undefined' &&
windowPresets[currentPresetName].perslice === true) {
// get the preset for this slice
var offset = image.getSecondaryOffset(currentIndex);
wl = windowPresets[currentPresetName].wl[offset];
}
// regular case
if (!wl) {
// if no current, use first id
if (!currentWl) {
this.setWindowLevelPresetById(0, true);
}
wl = currentWl;
}
// get the window lut
var wlut = windowLuts[rsi.toString()];
if (typeof wlut === 'undefined') {
// create the rescale lookup table
var rescaleLut = new dwv.image.RescaleLut(
image.getRescaleSlopeAndIntercept(0), image.getMeta().BitsStored);
// create the window lookup table
var windowLut = new dwv.image.WindowLut(
rescaleLut, image.getMeta().IsSigned);
// store
this.addWindowLut(windowLut);
wlut = windowLut;
}
// update lut window level if not present or different from previous
var lutWl = wlut.getWindowLevel();
if (!wl.equals(lutWl)) {
// set lut window level
wlut.setWindowLevel(wl);
wlut.update();
// fire change event
if (!lutWl ||
lutWl.getWidth() !== wl.getWidth() ||
lutWl.getCenter() !== wl.getCenter()) {
fireEvent({
type: 'wlchange',
value: [wl.getCenter(), wl.getWidth()],
wc: wl.getCenter(),
ww: wl.getWidth(),
skipGenerate: true
});
}
}
// return
return wlut;
};
/**
* Add the window LUT to the list.
*
* @param {Window} wlut The window LUT of the image.
*/
this.addWindowLut = function (wlut) {
var rsi = wlut.getRescaleLut().getRSI();
windowLuts[rsi.toString()] = wlut;
};
/**
* Get the window presets.
*
* @returns {object} The window presets.
*/
this.getWindowPresets = function () {
return windowPresets;
};
/**
* Get the window presets names.
*
* @returns {object} The list of window presets names.
*/
this.getWindowPresetsNames = function () {
return Object.keys(windowPresets);
};
/**
* Set the window presets.
*
* @param {object} presets The window presets.
*/
this.setWindowPresets = function (presets) {
windowPresets = presets;
};
/**
* Set the default colour map.
*
* @param {object} map The colour map.
*/
this.setDefaultColourMap = function (map) {
colourMap = map;
};
/**
* Add window presets to the existing ones.
*
* @param {object} presets The window presets.
*/
this.addWindowPresets = function (presets) {
var keys = Object.keys(presets);
var key = null;
for (var i = 0; i < keys.length; ++i) {
key = keys[i];
if (typeof windowPresets[key] !== 'undefined') {
if (typeof windowPresets[key].perslice !== 'undefined' &&
windowPresets[key].perslice === true) {
throw new Error('Cannot add perslice preset');
} else {
windowPresets[key] = presets[key];
}
} else {
// add new
windowPresets[key] = presets[key];
// fire event
/**
* Window/level add preset event.
*
* @event dwv.image.View#wlpresetadd
* @type {object}
* @property {string} name The name of the preset.
*/
fireEvent({
type: 'wlpresetadd',
name: key
});
}
}
};
/**
* Get the colour map of the image.
*
* @returns {object} The colour map of the image.
*/
this.getColourMap = function () {
return colourMap;
};
/**
* Set the colour map of the image.
*
* @param {object} map The colour map of the image.
* @fires dwv.image.View#colourchange
*/
this.setColourMap = function (map) {
colourMap = map;
/**
* Color change event.
*
* @event dwv.image.View#colourchange
* @type {object}
* @property {Array} value The changed value.
* @property {number} wc The new window center value.
* @property {number} ww The new window wdth value.
*/
fireEvent({
type: 'colourchange',
wc: this.getCurrentWindowLut().getWindowLevel().getCenter(),
ww: this.getCurrentWindowLut().getWindowLevel().getWidth()
});
};
/**
* Get the current position.
*
* @returns {dwv.math.Point} The current position.
*/
this.getCurrentPosition = function () {
return currentPosition;
};
/**
* Get the current index.
*
* @returns {dwv.math.Index} The current index.
*/
this.getCurrentIndex = function () {
var geometry = this.getImage().getGeometry();
return geometry.worldToIndex(currentPosition);
};
/**
* Set the current position.
*
* @param {dwv.math.Point} newPosition The new position.
* @param {boolean} silent Flag to fire event or not.
* @returns {boolean} False if not in bounds
* @fires dwv.image.View#positionchange
*/
this.setCurrentPosition = function (newPosition, silent) {
// check input
if (typeof silent === 'undefined') {
silent = false;
}
// check if possible
var geometry = image.getGeometry();
if (!geometry.isInBounds(newPosition)) {
return false;
}
var isNew = !currentPosition || !currentPosition.equals(newPosition);
if (isNew) {
var posIndex = geometry.worldToIndex(newPosition);
var diffDims = null;
if (currentPosition) {
if (currentPosition.canCompare(newPosition)) {
diffDims = currentPosition.compare(newPosition);
} else {
diffDims = [];
var minLen = Math.min(currentPosition.length(), newPosition.length());
for (var i = 0; i < minLen; ++i) {
if (currentPosition.get(i) !== newPosition.get(i)) {
diffDims.push(i);
}
}
var maxLen = Math.max(currentPosition.length(), newPosition.length());
for (var j = minLen; j < maxLen; ++j) {
diffDims.push(j);
}
}
} else {
diffDims = [];
for (var k = 0; k < newPosition.length(); ++k) {
diffDims.push(k);
}
}
// assign
currentPosition = newPosition;
if (!silent) {
/**
* Position change event.
*
* @event dwv.image.View#positionchange
* @type {object}
* @property {Array} value The changed value as [index, pixelValue].
* @property {Array} diffDims An array of modified indices.
*/
var posEvent = {
type: 'positionchange',
value: [
posIndex.getValues(),
currentPosition.getValues(),
],
diffDims: diffDims,
data: {
imageUid: image.getImageUid(posIndex)
}
};
// add value if possible
if (image.canQuantify()) {
var pixValue = image.getRescaledValueAtIndex(posIndex);
posEvent.value.push(pixValue);
}
// fire
fireEvent(posEvent);
}
}
// all good
return true;
};
/**
* Set the current index.
*
* @param {dwv.math.Index} index The index.
* @param {boolean} silent If true, does not fire a positionchange event.
* @returns {boolean} False if not in bounds.
*/
this.setCurrentIndex = function (index, silent) {
var geometry = this.getImage().getGeometry();
return this.setCurrentPosition(geometry.indexToWorld(index), silent);
};
/**
* Set the view window/level.
*
* @param {number} center The window center.
* @param {number} width The window width.
* @param {string} name Associated preset name, defaults to 'manual'.
* Warning: uses the latest set rescale LUT or the default linear one.
* @param {boolean} silent Flag to launch events with skipGenerate.
* @fires dwv.image.View#wlchange
*/
this.setWindowLevel = function (center, width, name, silent) {
// window width shall be >= 1 (see https://www.dabsoft.ch/dicom/3/C.11.2.1.2/)
if (width < 1) {
return;
}
// check input
if (typeof name === 'undefined') {
name = 'manual';
}
if (typeof silent === 'undefined') {
silent = false;
}
// new window level
var newWl = new dwv.image.WindowLevel(center, width);
// check if new
var isNew = !newWl.equals(currentWl);
// compare to previous if present
if (isNew) {
var isNewWidth = currentWl ? currentWl.getWidth() !== width : true;
var isNewCenter = currentWl ? currentWl.getCenter() !== center : true;
// assign
currentWl = newWl;
currentPresetName = name;
if (isNewWidth || isNewCenter) {
/**
* Window/level change event.
*
* @event dwv.image.View#wlchange
* @type {object}
* @property {Array} value The changed value.
* @property {number} wc The new window center value.
* @property {number} ww The new window wdth value.
* @property {boolean} skipGenerate Flag to skip view generation.
*/
fireEvent({
type: 'wlchange',
value: [center, width],
wc: center,
ww: width,
skipGenerate: silent
});
}
}
};
/**
* Set the window level to the preset with the input name.
*
* @param {string} name The name of the preset to activate.
* @param {boolean} silent Flag to launch events with skipGenerate.
*/
this.setWindowLevelPreset = function (name, silent) {
var preset = this.getWindowPresets()[name];
if (typeof preset === 'undefined') {
throw new Error('Unknown window level preset: \'' + name + '\'');
}
// special min/max
if (name === 'minmax' && typeof preset.wl === 'undefined') {
preset.wl = [this.getWindowLevelMinMax()];
}
// default to first
var wl = preset.wl[0];
// check if 'perslice' case
if (typeof preset.perslice !== 'undefined' &&
preset.perslice === true) {
var offset = image.getSecondaryOffset(this.getCurrentIndex());
wl = preset.wl[offset];
}
// set w/l
this.setWindowLevel(
wl.getCenter(), wl.getWidth(), name, silent);
};
/**
* Set the window level to the preset with the input id.
*
* @param {number} id The id of the preset to activate.
* @param {boolean} silent Flag to launch events with skipGenerate.
*/
this.setWindowLevelPresetById = function (id, silent) {
var keys = Object.keys(this.getWindowPresets());
this.setWindowLevelPreset(keys[id], silent);
};
/**
* Clone the image using all meta data and the original data buffer.
*
* @returns {dwv.image.View} A full copy of this {dwv.image.View}.
*/
this.clone = function () {
var copy = new dwv.image.View(this.getImage());
for (var key in windowLuts) {
copy.addWindowLut(windowLuts[key]);
}
copy.setListeners(this.getListeners());
return copy;
};
/**
* 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);
}
};
/**
* Get the image window/level that covers the full data range.
* Warning: uses the latest set rescale LUT or the default linear one.
*
* @returns {object} A min/max window level.
*/
dwv.image.View.prototype.getWindowLevelMinMax = function () {
var range = this.getImage().getRescaledDataRange();
var min = range.min;
var max = range.max;
var width = max - min;
// full black / white images, defaults to 1.
if (width < 1) {
dwv.logger.warn('Zero or negative width, defaulting to one.');
width = 1;
}
var center = min + width / 2;
return new dwv.image.WindowLevel(center, width);
};
/**
* Set the image window/level to cover the full data range.
* Warning: uses the latest set rescale LUT or the default linear one.
*/
dwv.image.View.prototype.setWindowLevelMinMax = function () {
// calculate center and width
var wl = this.getWindowLevelMinMax();
// set window level
this.setWindowLevel(wl.getCenter(), wl.getWidth(), 'minmax');
};
/**
* Generate display image data to be given to a canvas.
*
* @param {Array} array The array to fill in.
*/
dwv.image.View.prototype.generateImageData = function (array) {
// check position
if (!this.getCurrentPosition()) {
this.setInitialPosition();
}
var image = this.getImage();
var position = this.getCurrentIndex();
var iterator = dwv.image.getSliceIterator(
image, position, false, this.getOrientation());
var photoInterpretation = image.getPhotometricInterpretation();
switch (photoInterpretation) {
case 'MONOCHROME1':
case 'MONOCHROME2':
dwv.image.generateImageDataMonochrome(
array,
iterator,
this.getAlphaFunction(),
this.getCurrentWindowLut(),
this.getColourMap()
);
break;
case 'PALETTE COLOR':
dwv.image.generateImageDataPaletteColor(
array,
iterator,
this.getAlphaFunction(),
this.getColourMap(),
image.getMeta().BitsStored === 16
);
break;
case 'RGB':
dwv.image.generateImageDataRgb(
array,
iterator,
this.getAlphaFunction(),
this.getCurrentWindowLut()
);
break;
case 'YBR_FULL':
dwv.image.generateImageDataYbrFull(
array,
iterator,
this.getAlphaFunction()
);
break;
default:
throw new Error(
'Unsupported photometric interpretation: ' + photoInterpretation);
}
};
/**
* Increment the provided dimension.
*
* @param {number} dim The dimension to increment.
* @param {boolean} silent Do not send event.
* @returns {boolean} False if not in bounds.
*/
dwv.image.View.prototype.incrementIndex = function (dim, silent) {
var index = this.getCurrentIndex();
var values = new Array(index.length());
values.fill(0);
if (dim < values.length) {
values[dim] = 1;
} else {
console.warn('Cannot increment given index: ', dim, values.length);
}
var incr = new dwv.math.Index(values);
var newIndex = index.add(incr);
var geometry = this.getImage().getGeometry();
return this.setCurrentPosition(geometry.indexToWorld(newIndex), silent);
};
/**
* Decrement the provided dimension.
*
* @param {number} dim The dimension to increment.
* @param {boolean} silent Do not send event.
* @returns {boolean} False if not in bounds.
*/
dwv.image.View.prototype.decrementIndex = function (dim, silent) {
var index = this.getCurrentIndex();
var values = new Array(index.length());
values.fill(0);
if (dim < values.length) {
values[dim] = -1;
} else {
console.warn('Cannot decrement given index: ', dim, values.length);
}
var incr = new dwv.math.Index(values);
var newIndex = index.add(incr);
var geometry = this.getImage().getGeometry();
return this.setCurrentPosition(geometry.indexToWorld(newIndex), silent);
};
/**
* Get the scroll dimension index.
*
* @returns {number} The index.
*/
dwv.image.View.prototype.getScrollIndex = function () {
var index = null;
var orientation = this.getOrientation();
if (typeof orientation !== 'undefined') {
index = orientation.getThirdColMajorDirection();
} else {
index = 2;
}
return index;
};
/**
* Decrement the scroll dimension index.
*
* @param {boolean} silent Do not send event.
* @returns {boolean} False if not in bounds.
*/
dwv.image.View.prototype.decrementScrollIndex = function (silent) {
return this.decrementIndex(this.getScrollIndex(), silent);
};
/**
* Increment the scroll dimension index.
*
* @param {boolean} silent Do not send event.
* @returns {boolean} False if not in bounds.
*/
dwv.image.View.prototype.incrementScrollIndex = function (silent) {
return this.incrementIndex(this.getScrollIndex(), silent);
};