// Do not warn if these variables were not defined before.
/* global dwv */
// call setup on DOM loaded
document.addEventListener('DOMContentLoaded', onDOMContentLoaded);
let _app = null;
let _tools = null;
// tool features
const _toolFeaturesUI = {};
_toolFeaturesUI.Draw = {
getValue() {
const shapeSelect = document.getElementById('draw-shape-select');
return {
shapeName: shapeSelect.value
};
},
getHtml() {
const shapeSelect = document.createElement('select');
shapeSelect.id = 'draw-shape-select';
const shapeNames = _tools.Draw.options;
if (typeof shapeNames === 'undefined') {
return;
}
for (const shapeName of shapeNames) {
const opt = document.createElement('option');
opt.id = 'shape-' + shapeName;
opt.value = shapeName;
opt.appendChild(document.createTextNode(shapeName));
shapeSelect.appendChild(opt);
}
shapeSelect.onchange = function (event) {
const element = event.target;
_app.setToolFeatures({shapeName: element.value});
};
const autoColourInput = document.createElement('input');
autoColourInput.type = 'checkbox';
autoColourInput.id = 'draw-auto-colour';
autoColourInput.checked = true;
const autoLabel = document.createElement('label');
autoLabel.htmlFor = autoColourInput.id;
autoLabel.appendChild(document.createTextNode('auto colour'));
const colourInput = document.createElement('input');
colourInput.type = 'color';
colourInput.id = 'draw-colour-chooser';
colourInput.value = '#ffff80';
colourInput.disabled = true;
autoColourInput.onchange = function (event) {
const element = event.target;
_app.setToolFeatures({autoShapeColour: element.checked});
colourInput.disabled = element.checked;
};
colourInput.onchange = function (event) {
const element = event.target;
_app.setToolFeatures({shapeColour: element.value});
};
const res = document.createElement('span');
res.id = 'toolFeatures';
res.className = 'toolFeatures';
res.appendChild(shapeSelect);
res.appendChild(autoColourInput);
res.appendChild(autoLabel);
res.appendChild(colourInput);
return res;
}
};
// viewer options
let _layout = 'one';
/**
* Setup simple dwv app.
*/
function viewerSetup() {
// logger level (optional)
dwv.logger.level = dwv.logger.levels.DEBUG;
dwv.decoderScripts.jpeg2000 =
'../../decoders/pdfjs/decode-jpeg2000.js';
dwv.decoderScripts['jpeg-lossless'] =
'../../decoders/rii-mango/decode-jpegloss.js';
dwv.decoderScripts['jpeg-baseline'] =
'../../decoders/pdfjs/decode-jpegbaseline.js';
dwv.decoderScripts.rle =
'../../decoders/dwv/decode-rle.js';
dwv.defaultPresets.PT = {
'suv5-10': new dwv.WindowLevel(5, 10),
'suv6-8': new dwv.WindowLevel(6, 8)
};
// // example private logic for roi dialog
// dwv.customUI.openRoiDialog = function (meta, cb) {
// console.log('roi dialog', meta);
// const textExpr = prompt('[Custom dialog] Label', meta.textExpr);
// if (textExpr !== null) {
// meta.textExpr = textExpr;
// cb(meta);
// }
// };
// // example private logic for time value retrieval
// dwv.TagValueExtractor.prototype.getTime = function (elements) {
// let value;
// const time = elements['ABCD0123'];
// if (typeof time !== 'undefined') {
// value = parseInt(time.value[0], 10);
// }
// return value;
// };
// // example labelText override
// dwv.defaults.labelText.rectangle = {
// '*': '{surface}!',
// MR: '{surface}!!'
// };
// stage options
let viewOnFirstLoadItem = true;
// use for concurrent load
const numberOfDataToLoad = 1;
if (_layout === 'one') {
// simplest: one layer group
addLayerGroups(1);
} else if (_layout === 'side') {
// side by side
addLayerGroups(2);
} else if (_layout === 'mpr') {
// MPR
viewOnFirstLoadItem = false;
addLayerGroups(3);
}
// tools
_tools = {
Scroll: {},
WindowLevel: {},
ZoomAndPan: {},
Opacity: {},
Draw: {options: [
'Arrow',
'Ruler',
'Circle',
'Ellipse',
'Rectangle',
'Protractor',
'Roi',
'FreeHand'
]}
};
// app config
const options = new dwv.AppOptions();
options.tools = _tools;
options.viewOnFirstLoadItem = viewOnFirstLoadItem;
// app
_app = new dwv.App();
_app.init(options);
// abort shortcut handler
const abortShortcut = function (event) {
if (event.key === 'a') {
_app.abortAllLoads();
}
};
// bind events
_app.addEventListener('error', function (event) {
console.error('load error', event);
// abort load
_app.abortLoad(event.dataid);
});
_app.addEventListener('loadstart', function (event) {
console.time('load-data-' + event.dataid);
// add abort shortcut
window.addEventListener('keydown', abortShortcut);
// update data view config
const dataIds = [event.dataid];
let configs;
if (_layout === 'one') {
configs = getOnebyOneDataViewConfig(dataIds);
} else if (_layout === 'side') {
configs = getOnebyTwoDataViewConfig(dataIds);
} else if (_layout === 'mpr') {
configs = getMPRDataViewConfig(dataIds);
}
const viewConfigs = configs[event.dataid];
for (let i = 0; i < viewConfigs.length; ++i) {
_app.addDataViewConfig(event.dataid, viewConfigs[i]);
}
});
const dataLoadProgress = new Array(numberOfDataToLoad);
const sumReducer = function (sum, value) {
return sum + value;
};
_app.addEventListener('loadprogress', function (event) {
if (typeof event.lengthComputable !== 'undefined' &&
event.lengthComputable) {
dataLoadProgress[event.dataid] =
Math.ceil((event.loaded / event.total) * 100);
const progressElement = document.getElementById('loadprogress');
progressElement.value =
dataLoadProgress.reduce(sumReducer) / numberOfDataToLoad;
}
});
_app.addEventListener('load', function (event) {
if (!viewOnFirstLoadItem) {
_app.render(event.dataid);
}
});
_app.addEventListener('loaditem', function (event) {
if (typeof event.warn !== 'undefined') {
console.warn('load-warn', event.warn);
}
});
_app.addEventListener('loadend', function (event) {
console.timeEnd('load-data-' + event.dataid);
// remove abort shortcut
window.removeEventListener('keydown', abortShortcut);
});
let dataLoad = 0;
const firstRender = [];
_app.addEventListener('load', function (event) {
// update UI at first render
if (!firstRender.includes(event.dataid)) {
// store data id
firstRender.push(event.dataid);
// log meta data
if (event.loadtype === 'image') {
console.log('metadata',
getMetaDataWithNames(_app.getMetaData(event.dataid)));
// add data row
addDataRow(event.dataid);
++dataLoad;
// init gui
if (dataLoad === numberOfDataToLoad) {
// set app tool
setAppTool();
const toolsFieldset = document.getElementById('tools');
toolsFieldset.disabled = false;
const changeLayoutSelect = document.getElementById('changelayout');
changeLayoutSelect.disabled = false;
const resetLayoutButton = document.getElementById('resetlayout');
resetLayoutButton.disabled = false;
const smoothingChk = document.getElementById('changesmoothing');
smoothingChk.disabled = false;
}
}
}
const meta = _app.getMetaData(event.dataid);
if (event.loadtype === 'image' &&
typeof meta['00080060'] !== 'undefined' &&
meta['00080060'].value[0] === 'SEG') {
// log SEG details
logFramePosPats(_app.getMetaData(event.dataid));
// example usage of a dicom SEG as data mask
const useSegAsMask = false;
if (useSegAsMask) {
// image to filter
const dataId = 0;
const vls = _app.getViewLayersByDataId(dataId);
const vc = vls[0].getViewController();
const img = _app.getImage(dataId);
const imgGeometry = img.getGeometry();
const sliceSize = imgGeometry.getSize().getDimSize(2);
// SEG image
const segImage = _app.getImage(event.dataid);
// calculate slice difference
const segOrigin0 = segImage.getGeometry().getOrigins()[0];
const segOrigin0Point = new dwv.Point([
segOrigin0.getX(), segOrigin0.getY(), segOrigin0.getZ()
]);
const segOriginIndex = imgGeometry.worldToIndex(segOrigin0Point);
const z = segOriginIndex.get(2);
if (typeof z !== 'undefined') {
const indexOffset = z * sliceSize;
// set alpha function
vc.setViewAlphaFunction(function (value, index) {
// multiply by 3 since SEG is RGB
const segIndex = 3 * (index - indexOffset);
if (segIndex >= 0 &&
segImage.getValueAtOffset(segIndex) === 0 &&
segImage.getValueAtOffset(segIndex + 1) === 0 &&
segImage.getValueAtOffset(segIndex + 2) === 0) {
return 0;
} else {
return 0xff;
}
});
}
}
}
});
_app.addEventListener('positionchange', function (event) {
const input = document.getElementById('position');
const values = event.value[1];
let text = '(index: ' + event.value[0] + ')';
if (event.value.length > 2) {
text += ' value: ' + event.value[2];
}
input.value = values.map(getPrecisionRound(2));
// index as small text
const span = document.getElementById('positionspan');
if (span) {
span.innerHTML = text;
}
});
// default keyboard shortcuts
window.addEventListener('keydown', function (event) {
_app.defaultOnKeydown(event);
// mask segment related
if (!isNaN(parseInt(event.key, 10))) {
const lg = _app.getActiveLayerGroup();
const vl = lg.getActiveViewLayer();
if (typeof vl === 'undefined') {
return;
}
const vc = vl.getViewController();
if (!vc.isMask()) {
return;
}
const number = parseInt(event.key, 10);
const segHelper = vc.getMaskSegmentHelper();
if (segHelper.hasSegment(number)) {
const segment = segHelper.getSegment(number);
if (event.ctrlKey) {
if (event.altKey) {
console.log('Delete segment: ' + segment.label);
// delete
vc.deleteSegment(number, _app.addToUndoStack);
} else {
console.log('Show/hide segment: ' + segment.label);
// show/hide the selected segment
if (segHelper.isHidden(number)) {
segHelper.removeFromHidden(number);
} else {
segHelper.addToHidden(number);
}
vc.applyHiddenSegments();
}
}
}
}
});
// default on resize
window.addEventListener('resize', function () {
_app.onResize();
});
const uriOptions = {};
// uriOptions.batchSize = 100;
// special dicom web cookie
if (document.cookie) {
const cookies = document.cookie.split('; ');
// accept
const acceptItem = cookies.find((item) => item.startsWith('accept='));
if (typeof acceptItem !== 'undefined') {
// accept is encoded in dcmweb.js (allows for ';')
const accept = decodeURIComponent(acceptItem.split('=')[1]);
if (typeof accept !== 'undefined' && accept.length !== 0) {
uriOptions.requestHeaders = [];
uriOptions.requestHeaders.push({
name: 'Accept',
value: accept
});
}
// clean up
document.cookie = 'accept=';
}
// token
const tokenItem = cookies.find((item) => item.startsWith('access_token='));
if (typeof tokenItem !== 'undefined') {
const token = tokenItem.split('=')[1];
if (typeof token !== 'undefined' && token.length !== 0) {
if (typeof uriOptions.requestHeaders === 'undefined') {
uriOptions.requestHeaders = [];
}
uriOptions.requestHeaders.push({
name: 'Authorization',
value: 'Bearer ' + token
});
}
// clean up
document.cookie = 'access_token=';
}
}
// load from window location
_app.loadFromUri(window.location.href, uriOptions);
}
/**
* Setup.
*/
function onDOMContentLoaded() {
// setup
viewerSetup();
const positionInput = document.getElementById('position');
positionInput.addEventListener('change', function (event) {
const vls = _app.getViewLayersByDataId('0');
const vc = vls[0].getViewController();
const element = event.target;
const values = element.value.split(',');
vc.setCurrentPosition(new dwv.Point([
parseFloat(values[0]), parseFloat(values[1]), parseFloat(values[2])
])
);
});
const resetLayoutButton = document.getElementById('resetlayout');
resetLayoutButton.disabled = true;
resetLayoutButton.addEventListener('click', function () {
_app.resetLayout();
});
const changeLayoutSelect = document.getElementById('changelayout');
changeLayoutSelect.disabled = true;
changeLayoutSelect.addEventListener('change', function (event) {
const selectElement = event.target;
const layout = selectElement.value;
if (layout !== 'one' &&
layout !== 'side' &&
layout !== 'mpr') {
throw new Error('Unknown layout: ' + layout);
}
_layout = layout;
let configs;
const dataIds = _app.getDataIds();
if (layout === 'one') {
addLayerGroups(1);
configs = getOnebyOneDataViewConfig(dataIds);
} else if (layout === 'side') {
addLayerGroups(2);
configs = getOnebyTwoDataViewConfig(dataIds);
} else if (layout === 'mpr') {
addLayerGroups(3);
configs = getMPRDataViewConfig(dataIds);
}
if (typeof configs === 'undefined') {
return;
}
// unbind app to controls
unbindAppToControls();
// set config
_app.setDataViewConfigs(configs);
clearDataTable();
for (let i = 0; i < dataIds.length; ++i) {
_app.render(dataIds[i]);
// add data row (will bind controls)
addDataRow(dataIds[i]);
}
// show crosshair depending on layout
if (layout !== 'one') {
const divIds = getLayerGroupDivIds(configs);
for (const divId of divIds) {
_app.getLayerGroupByDivId(divId).setShowCrosshair(true);
}
}
// need to set tool after config change
setAppTool();
});
const smoothingChk = document.getElementById('changesmoothing');
smoothingChk.checked = false;
smoothingChk.disabled = true;
smoothingChk.addEventListener('change', function (event) {
const inputElement = event.target;
_app.setImageSmoothing(inputElement.checked);
});
// setup
setupBindersCheckboxes();
setupToolsCheckboxes();
setupTests();
setupAbout();
// bind app to input files
const fileinput = document.getElementById('fileinput');
fileinput.addEventListener('change', function (event) {
console.log('%c ----------------', 'color: teal;');
const fileElement = event.target;
console.log(fileElement.files);
_app.loadFiles(fileElement.files);
});
}
/**
* Append a layer div in the root 'dwv' one.
*
* @param {string} id The id of the layer.
*/
function addLayerGroup(id) {
const layerDiv = document.createElement('div');
layerDiv.id = id;
layerDiv.className = 'layerGroup';
const root = document.getElementById('dwv');
root.appendChild(layerDiv);
}
/**
* Add Layer Groups.
*
* @param {number} number The number of layer groups.
*/
function addLayerGroups(number) {
// clean up
const dwvDiv = document.getElementById('dwv');
if (dwvDiv) {
dwvDiv.innerHTML = '';
}
// add div
for (let i = 0; i < number; ++i) {
addLayerGroup('layerGroup' + i);
}
}
/**
* Get a full view for a given div id.
*
* @param {string} divId The div id.
* @returns {object} The config.
*/
function getViewConfig(divId) {
const config = {divId: divId};
if (_layout === 'mpr') {
if (divId === 'layerGroup0') {
config.orientation = dwv.Orientation.Axial;
} else if (divId === 'layerGroup1') {
config.orientation = dwv.Orientation.Coronal;
} else if (divId === 'layerGroup2') {
config.orientation = dwv.Orientation.Sagittal;
}
}
return config;
}
/**
* Merge an app data config into the input one.
* Copies all but the divId and orientation property.
*
* @param {string} dataId The data id.
* @param {object} config The view config.
* @returns {object} The update config.
*/
function mergeDataConfig(dataId, config) {
const oldConfigs = _app.getViewConfigs(dataId);
if (oldConfigs.length !== 0) {
// use first config as base
const oldConfig = oldConfigs[0];
for (const key in oldConfig) {
if (key !== 'divId' &&
key !== 'orientation') {
config[key] = oldConfig[key];
}
}
}
return config;
}
/**
* Create 1*2 view config(s).
*
* @param {Array} dataIds The list of dataIds.
* @returns {object} The view config.
*/
function getOnebyOneDataViewConfig(dataIds) {
const configs = {};
for (const dataId of dataIds) {
configs[dataId] =
[mergeDataConfig(dataId, getViewConfig('layerGroup0'))];
}
return configs;
}
/**
* Create 1*2 view config(s).
*
* @param {Array} dataIds The list of dataIds.
* @returns {object} The view config.
*/
function getOnebyTwoDataViewConfig(dataIds) {
const configs = {};
for (let i = 0; i < dataIds.length; ++i) {
const dataId = dataIds[i];
let config;
if (i % 2 === 0) {
config = getViewConfig('layerGroup0');
} else {
config = getViewConfig('layerGroup1');
}
configs[dataIds[i]] = [mergeDataConfig(dataId, config)];
}
return configs;
}
/**
* Get MPR view config(s).
*
* @param {Array} dataIds The list of dataIds.
* @returns {object} The view config.
*/
function getMPRDataViewConfig(dataIds) {
const configs = {};
for (const dataId of dataIds) {
configs[dataId] = [
mergeDataConfig(dataId, getViewConfig('layerGroup0')),
mergeDataConfig(dataId, getViewConfig('layerGroup1')),
mergeDataConfig(dataId, getViewConfig('layerGroup2'))
];
}
return configs;
}
/**
* Get the layer groups div ids from the data view configs.
*
* @param {object} dataViewConfigs The configs.
* @returns {Array} The list of ids.
*/
function getLayerGroupDivIds(dataViewConfigs) {
const divIds = [];
const keys = Object.keys(dataViewConfigs);
for (let i = 0; i < keys.length; ++i) {
const dataViewConfig = dataViewConfigs[keys[i]];
for (let j = 0; j < dataViewConfig.length; ++j) {
const divId = dataViewConfig[j].divId;
if (!divIds.includes(divId)) {
divIds.push(divId);
}
}
}
return divIds;
}
/**
* Get the layer group div ids associated to a view config.
*
* @param {Array} dataViewConfig The data view config.
* @returns {Array} The list of div ids.
*/
function getDivIds(dataViewConfig) {
const divIds = [];
for (let j = 0; j < dataViewConfig.length; ++j) {
divIds.push(dataViewConfig[j].divId);
}
return divIds;
}
/**
* Get the layer group div ids associated to a data id.
*
* @param {string} dataId The data id.
* @returns {Array} The list of div ids.
*/
function getDataLayerGroupDivIds(dataId) {
const dataViewConfigs = _app.getDataViewConfigs();
let viewConfig = dataViewConfigs[dataId];
if (typeof viewConfig === 'undefined') {
viewConfig = dataViewConfigs['*'];
}
return getDivIds(viewConfig);
}
/**
* Setup the binders checkboxes.
*/
function setupBindersCheckboxes() {
const propList = [
'WindowLevel',
'Position',
'Zoom',
'Offset',
'Opacity',
'ColourMap'
];
const binders = [];
// add all binders at startup
for (let b = 0; b < propList.length; ++b) {
binders.push(propList[b] + 'Binder');
}
_app.setLayerGroupsBinders(binders);
/**
* Add a binder.
*
* @param {string} propName The name of the property to bind.
*/
function addBinder(propName) {
binders.push(propName + 'Binder');
_app.setLayerGroupsBinders(binders);
}
/**
* Remove a binder.
*
* @param {string} propName The name of the property to bind.
*/
function removeBinder(propName) {
const index = binders.indexOf(propName + 'Binder');
if (index !== -1) {
binders.splice(index, 1);
}
_app.setLayerGroupsBinders(binders);
}
/**
* Get the input change handler for a binder.
*
* @param {string} propName The name of the property to bind.
* @returns {object} The handler.
*/
function getOnInputChange(propName) {
return function (event) {
const inputElement = event.target;
if (inputElement.checked) {
addBinder(propName);
} else {
removeBinder(propName);
}
};
}
const fieldset = document.getElementById('binders');
// individual binders
for (let i = 0; i < propList.length; ++i) {
const propName = propList[i];
const input = document.createElement('input');
input.id = 'binder-' + i;
input.type = 'checkbox';
input.checked = true;
input.onchange = getOnInputChange(propName);
const label = document.createElement('label');
label.htmlFor = input.id;
label.appendChild(document.createTextNode(propName));
fieldset.appendChild(input);
fieldset.appendChild(label);
}
// check all
const allInput = document.createElement('input');
allInput.id = 'binder-all';
allInput.type = 'checkbox';
allInput.checked = true;
allInput.onchange = function () {
for (let j = 0; j < propList.length; ++j) {
document.getElementById('binder-' + j).click();
}
};
const allLabel = document.createElement('label');
allLabel.htmlFor = allInput.id;
allLabel.appendChild(document.createTextNode('all'));
fieldset.appendChild(allInput);
fieldset.appendChild(allLabel);
}
/**
* Setup the tools checkboxes.
*/
function setupToolsCheckboxes() {
const getChangeTool = function (tool) {
return function () {
setAppTool(tool);
};
};
const getKeyCheck = function (char, input) {
return function (event) {
if (!event.ctrlKey &&
!event.altKey &&
!event.shiftKey &&
event.key === char) {
input.click();
}
};
};
const fieldset = document.getElementById('tools');
const keys = Object.keys(_tools);
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
const input = document.createElement('input');
input.name = 'tools';
input.type = 'radio';
input.id = 'tool-' + i;
input.title = key;
input.onchange = getChangeTool(key);
if (key === 'Scroll') {
input.checked = true;
}
const label = document.createElement('label');
label.htmlFor = input.id;
label.title = input.title;
label.appendChild(document.createTextNode(input.title));
fieldset.appendChild(input);
fieldset.appendChild(label);
// keyboard shortcut
const shortcut = key[0].toLowerCase();
window.addEventListener('keydown', getKeyCheck(shortcut, input));
}
// tool options
const div = document.createElement('div');
div.id = 'toolOptions';
fieldset.appendChild(div);
}
/**
* Set the app tool.
*
* @param {string} [toolName] The tool to set.
*/
function setAppTool(toolName) {
// find the tool name if not provided
if (typeof toolName === 'undefined') {
const toolsInput = document.getElementsByName('tools');
for (let j = 0; j < toolsInput.length; ++j) {
const toolInput = toolsInput[j];
if (toolInput.checked) {
toolName = toolInput.title;
break;
}
}
if (typeof toolName === 'undefined') {
console.warn('Cannot find tool to set the app with...');
return;
}
}
// set tool for app
_app.setTool(toolName);
// clear options html
const toolOptionsEl = document.getElementById('toolOptions');
if (toolOptionsEl !== null) {
toolOptionsEl.innerHTML = '';
}
// tool features
const featuresUI = _toolFeaturesUI[toolName];
if (toolOptionsEl !== null &&
typeof featuresUI !== 'undefined') {
// setup html
const featuresHtml = featuresUI.getHtml();
if (typeof featuresHtml !== 'undefined') {
toolOptionsEl.appendChild(featuresHtml);
}
// pass value to app
const features = featuresUI.getValue();
if (typeof features !== 'undefined') {
_app.setToolFeatures(features);
}
}
}
/**
* Bind app to controls.
*/
function bindAppToControls() {
_app.addEventListener('wlchange', onWLChange);
_app.addEventListener('opacitychange', onOpacityChange);
}
/**
* Unbind app to controls.
*/
function unbindAppToControls() {
_app.removeEventListener('wlchange', onWLChange);
_app.removeEventListener('opacitychange', onOpacityChange);
}
/**
* Handle app wl change.
*
* @param {object} event The change event.
*/
function onWLChange(event) {
// width number
let elemId = 'width-' + event.dataid + '-number';
let elem = document.getElementById(elemId);
if (elem) {
elem.value = event.value[1];
} else {
console.warn('wl change: HTML not ready?');
}
// width range
elemId = 'width-' + event.dataid + '-range';
elem = document.getElementById(elemId);
if (elem) {
elem.value = event.value[1];
}
// center number
elemId = 'center-' + event.dataid + '-number';
elem = document.getElementById(elemId);
if (elem) {
elem.value = event.value[0];
}
// center range
elemId = 'center-' + event.dataid + '-range';
elem = document.getElementById(elemId);
if (elem) {
elem.value = event.value[0];
}
// preset select
elemId = 'preset-' + event.dataid + '-select';
const selectElem = document.getElementById(elemId);
if (selectElem) {
const ids = getDataLayerGroupDivIds(event.dataid);
const lg = _app.getLayerGroupByDivId(ids[0]);
const vls = lg.getViewLayersByDataId(event.dataid);
if (typeof vls !== 'undefined' && vls.length !== 0) {
const vl = vls[0];
const vc = vl.getViewController();
const presetName = vc.getCurrentWindowPresetName();
const optName = 'manual';
if (presetName === optName) {
const options = selectElem.options;
const optId = 'preset-manual';
let manualOpt = options.namedItem(optId);
if (!manualOpt) {
const opt = document.createElement('option');
opt.id = optId;
opt.value = optName;
opt.appendChild(document.createTextNode(optName));
manualOpt = selectElem.appendChild(opt);
}
selectElem.selectedIndex = manualOpt.index;
}
}
}
}
/**
* Handle app opacity change.
*
* @param {object} event The change event.
*/
function onOpacityChange(event) {
const value = parseFloat(event.value[0]).toPrecision(3);
// number
let elemId = 'opacity-' + event.dataid + '-number';
let elem = document.getElementById(elemId);
if (elem) {
elem.value = value;
} else {
console.warn('opacity change: HTML not ready?');
}
// range
elemId = 'opacity-' + event.dataid + '-range';
elem = document.getElementById(elemId);
if (elem) {
elem.value = value;
}
}
/**
* Clear the data table.
*/
function clearDataTable() {
const detailsDiv = document.getElementById('layersdetails');
if (detailsDiv) {
detailsDiv.innerHTML = '';
}
}
/**
* Get a control div: label, range and number field.
*
* @param {string} id The control id.
* @param {string} name The control name.
* @param {number} min The control minimum value.
* @param {number} max The control maximum value.
* @param {number} value The control value.
* @param {Function} callback The callback on control value change.
* @param {number} precision Optional number field float precision.
* @returns {object} The control div.
*/
function getControlDiv(
id,
name,
min,
max,
value,
callback,
precision) {
const range = document.createElement('input');
range.id = id + '-range';
range.className = 'ctrl-range';
range.type = 'range';
range.min = min.toPrecision(precision);
range.max = max.toPrecision(precision);
range.step = ((max - min) * 0.01).toPrecision(precision);
range.value = value.toString();
const label = document.createElement('label');
label.id = id + '-label';
label.className = 'ctrl-label';
label.htmlFor = range.id;
label.appendChild(document.createTextNode(name));
const number = document.createElement('input');
number.id = id + '-number';
number.className = 'ctrl-number';
number.type = 'number';
number.min = range.min;
number.max = range.max;
number.step = range.step;
number.value = value.toPrecision(precision);
// callback and bind range and number
number.oninput = function (event) {
const element = event.target;
range.value = element.value;
callback(element.value);
};
range.oninput = function (event) {
const element = event.target;
number.value = parseFloat(element.value).toPrecision(precision);
callback(element.value);
};
const div = document.createElement('div');
div.id = id + '-ctrl';
div.className = 'ctrl';
div.appendChild(label);
div.appendChild(range);
div.appendChild(number);
return div;
}
/**
* Add a data row.
*
* @param {string} dataId The data id.
*/
function addDataRow(dataId) {
// bind app to controls on first id
if (dataId === '0') {
bindAppToControls();
}
const dataViewConfigs = _app.getDataViewConfigs();
const allLayerGroupDivIds = getLayerGroupDivIds(dataViewConfigs);
// use first view layer
const initialVls = _app.getViewLayersByDataId(dataId);
const initialVl = initialVls[0];
const initialVc = initialVl.getViewController();
const initialWl = initialVc.getWindowLevel();
let table = document.getElementById('layerstable');
let body;
// create table if not present
if (!table) {
table = document.createElement('table');
table.id = 'layerstable';
const header = table.createTHead();
const trow = header.insertRow(0);
const insertTCell = function (text) {
const th = document.createElement('th');
th.innerHTML = text;
trow.appendChild(th);
};
insertTCell('Id');
for (let j = 0; j < allLayerGroupDivIds.length; ++j) {
insertTCell('LG' + j);
}
insertTCell('Alpha Range');
insertTCell('Contrast');
insertTCell('Preset');
insertTCell('Alpha');
body = table.createTBody();
const div = document.getElementById('layersdetails');
div.appendChild(table);
} else {
body = table.getElementsByTagName('tbody')[0];
}
// add new layer row
const row = body.insertRow();
let cell;
// get the selected layer group ids
const getSelectedLayerGroupIds = function () {
const res = [];
for (let l = 0; l < allLayerGroupDivIds.length; ++l) {
const layerGroupDivId = allLayerGroupDivIds[l];
const elemId = 'layerselect-' + layerGroupDivId + '-' + dataId;
const elem = document.getElementById(elemId);
if (elem && elem.checked) {
res.push(layerGroupDivId);
}
}
return res;
};
// get a layer radio button
const getLayerRadio = function (index, divId) {
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'layerselect-' + index;
radio.id = 'layerselect-' + divId + '-' + dataId;
radio.checked = true;
radio.onchange = function (event) {
const element = event.target;
const fullId = element.id;
const split = fullId.split('-');
const groupDivId = split[1];
const dataId = split[2];
const lg = _app.getLayerGroupByDivId(groupDivId);
lg.setActiveDrawLayerByDataId(dataId);
lg.setActiveViewLayerByDataId(dataId);
};
return radio;
};
// get a layer add button
const getLayerAdd = function (index, divId) {
const button = document.createElement('button');
button.name = 'layeradd-' + index;
button.id = 'layeradd-' + divId + '-' + dataId;
button.title = 'Add layer';
button.appendChild(document.createTextNode('+'));
button.onclick = function () {
// update app
_app.addDataViewConfig(dataId, getViewConfig(divId));
// update html
const parent = button.parentElement;
if (parent) {
parent.replaceChildren();
parent.appendChild(getLayerRadio(index, divId));
parent.appendChild(getLayerRem(index, divId));
parent.appendChild(
getLayerUpdate(index, divId, dwv.Orientation.Axial));
parent.appendChild(
getLayerUpdate(index, divId, dwv.Orientation.Coronal));
parent.appendChild(
getLayerUpdate(index, divId, dwv.Orientation.Sagittal));
}
};
return button;
};
// get a layer remove button
const getLayerRem = function (index, divId) {
const button = document.createElement('button');
button.name = 'layerrem-' + index;
button.id = 'layerrem-' + divId + '-' + dataId;
button.title = 'Remove layer';
button.appendChild(document.createTextNode('-'));
button.onclick = function () {
// update app
_app.removeDataViewConfig(dataId, divId);
// update html
const parent = button.parentElement;
parent.replaceChildren();
parent.appendChild(getLayerAdd(index, divId));
};
return button;
};
// get a layer update button
const getLayerUpdate = function (index, divId, orientation) {
const button = document.createElement('button');
const letter = orientation[0].toUpperCase();
button.name = 'layerupd-' + index + '_' + letter;
button.id = 'layerupd-' + divId + '-' + dataId + '_' + letter;
button.title = 'Change layer orientation to ' + orientation;
button.appendChild(document.createTextNode(letter));
button.onclick = function () {
// update app
const config = getViewConfig(divId);
config.orientation = orientation;
_app.updateDataViewConfig(dataId, divId, config);
};
return button;
};
// cell: id
cell = row.insertCell();
cell.appendChild(document.createTextNode(dataId));
// cell: radio
let viewConfig = dataViewConfigs[dataId];
if (typeof viewConfig === 'undefined') {
viewConfig = dataViewConfigs['*'];
}
const dataLayerGroupsIds = getDivIds(viewConfig);
for (let l = 0; l < allLayerGroupDivIds.length; ++l) {
const layerGroupDivId = allLayerGroupDivIds[l];
cell = row.insertCell();
if (dataLayerGroupsIds.includes(layerGroupDivId)) {
cell.appendChild(getLayerRadio(l, layerGroupDivId));
cell.appendChild(getLayerRem(l, layerGroupDivId));
cell.appendChild(
getLayerUpdate(l, layerGroupDivId, dwv.Orientation.Axial));
cell.appendChild(
getLayerUpdate(l, layerGroupDivId, dwv.Orientation.Coronal));
cell.appendChild(
getLayerUpdate(l, layerGroupDivId, dwv.Orientation.Sagittal));
} else {
cell.appendChild(getLayerAdd(l, layerGroupDivId));
}
}
const image = _app.getImage(initialVl.getDataId());
const dataRange = image.getDataRange();
const rescaledDataRange = image.getRescaledDataRange();
const floatPrecision = 4;
// cell: alpha range
cell = row.insertCell();
const minId = 'value-min-' + dataId;
const maxId = 'value-max-' + dataId;
// callback
const changeAlphaFunc = function () {
const minElement = document.getElementById(minId + '-number');
const min = parseFloat(minElement.value);
const maxElement = document.getElementById(maxId + '-number');
const max = parseFloat(maxElement.value);
const func = function (value, _index) {
if (value >= min && value <= max) {
return 255;
}
return 0;
};
// update selected layers
const lgIds = getSelectedLayerGroupIds();
for (let i = 0; i < lgIds.length; ++i) {
const lg = _app.getLayerGroupByDivId(lgIds[i]);
const vl = lg.getActiveViewLayer();
if (typeof vl !== 'undefined') {
const vc = vl.getViewController();
vc.setViewAlphaFunction(func);
}
}
};
// add controls
cell.appendChild(getControlDiv(minId, 'min',
dataRange.min, dataRange.max, dataRange.min,
changeAlphaFunc, floatPrecision));
cell.appendChild(getControlDiv(maxId, 'max',
dataRange.min, dataRange.max, dataRange.max,
changeAlphaFunc, floatPrecision));
// cell: contrast
cell = row.insertCell();
const widthId = 'width-' + dataId;
const centerId = 'center-' + dataId;
// callback
const changeContrast = function () {
const wElement = document.getElementById(widthId + '-number');
const width = parseFloat(wElement.value);
const cElement = document.getElementById(centerId + '-number');
const center = parseFloat(cElement.value);
// update selected layers
const lgIds = getSelectedLayerGroupIds();
for (let i = 0; i < lgIds.length; ++i) {
const lg = _app.getLayerGroupByDivId(lgIds[i]);
const vl = lg.getActiveViewLayer();
if (typeof vl !== 'undefined') {
const vc = vl.getViewController();
vc.setWindowLevel(new dwv.WindowLevel(center, width));
}
}
};
// add controls
cell.appendChild(getControlDiv(widthId, 'width',
0, rescaledDataRange.max - rescaledDataRange.min, initialWl.width,
changeContrast, floatPrecision));
cell.appendChild(getControlDiv(centerId, 'center',
rescaledDataRange.min, rescaledDataRange.max, initialWl.center,
changeContrast, floatPrecision));
// cell: presets
cell = row.insertCell();
// window level preset
// callback
const changePreset = function (event) {
const element = event.target;
// update selected layers
const lgIds = getSelectedLayerGroupIds();
for (let i = 0; i < lgIds.length; ++i) {
const lg = _app.getLayerGroupByDivId(lgIds[i]);
const vl = lg.getActiveViewLayer();
if (typeof vl !== 'undefined') {
const vc = vl.getViewController();
vc.setWindowLevelPreset(element.value);
}
}
};
const selectPreset = document.createElement('select');
selectPreset.id = 'preset-' + dataId + '-select';
const presets = initialVc.getWindowLevelPresetsNames();
const currentPresetName = initialVc.getCurrentWindowPresetName();
for (const preset of presets) {
const option = document.createElement('option');
option.value = preset;
if (preset === currentPresetName) {
option.selected = true;
}
option.appendChild(document.createTextNode(preset));
selectPreset.appendChild(option);
}
selectPreset.onchange = changePreset;
const labelPreset = document.createElement('label');
labelPreset.htmlFor = selectPreset.id;
labelPreset.appendChild(document.createTextNode('wl: '));
cell.appendChild(labelPreset);
cell.appendChild(selectPreset);
// break line
const br = document.createElement('br');
cell.appendChild(br);
// colour map
// callback
const changeColourMap = function (event) {
const element = event.target;
// update selected layers
const lgIds = getSelectedLayerGroupIds();
for (let i = 0; i < lgIds.length; ++i) {
const lg = _app.getLayerGroupByDivId(lgIds[i]);
const vl = lg.getActiveViewLayer();
if (typeof vl !== 'undefined') {
const vc = vl.getViewController();
vc.setColourMap(element.value);
}
}
};
const selectColourMap = document.createElement('select');
selectColourMap.id = 'colourmap-' + dataId + '-select';
const colourMaps = Object.keys(dwv.luts);
const currentColourMap = initialVc.getColourMap();
for (const colourMap of colourMaps) {
const option = document.createElement('option');
option.value = colourMap;
if (colourMap === currentColourMap) {
option.selected = true;
}
option.appendChild(document.createTextNode(colourMap));
selectColourMap.appendChild(option);
}
selectColourMap.onchange = changeColourMap;
const labelColourMap = document.createElement('label');
labelColourMap.htmlFor = selectColourMap.id;
labelColourMap.appendChild(document.createTextNode('cm: '));
cell.appendChild(labelColourMap);
cell.appendChild(selectColourMap);
// cell: opactiy
cell = row.insertCell();
const opacityId = 'opacity-' + dataId;
// callback
const changeOpacity = function (value) {
// update selected layers
const lgIds = getSelectedLayerGroupIds();
for (let i = 0; i < lgIds.length; ++i) {
const lg = _app.getLayerGroupByDivId(lgIds[i]);
const vl = lg.getActiveViewLayer();
if (typeof vl !== 'undefined') {
vl.setOpacity(value);
vl.draw();
}
}
};
// add controls
cell.appendChild(getControlDiv(opacityId, 'opacity',
0, 1, initialVl.getOpacity(), changeOpacity, floatPrecision));
}
/**
* Compare two pos pat keys.
*
* @param {string} a The key of the first item.
* @param {string} b The key of the second item.
* @returns {number} Negative if a<b, positive if a>b.
*/
function comparePosPat(a, b) {
const za = a.split('\\').at(-1);
const zb = b.split('\\').at(-1);
let res = 0;
if (typeof za !== 'undefined' &&
typeof zb !== 'undefined') {
res = parseFloat(za) - parseFloat(zb);
}
return res;
}
/**
* Sort an object with pos pat string keys.
*
* @param {object} obj The object to sort.
* @returns {object} The sorted object.
*/
function sortByPosPatKey(obj) {
const keys = Object.keys(obj);
keys.sort(comparePosPat);
const sorted = new Map();
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
sorted.set(key, obj[key]);
}
return sorted;
}
/**
* Get a rounding function for a specific precision.
*
* @param {number} precision The rounding precision.
* @returns {Function} The rounding function.
*/
function getPrecisionRound(precision) {
return function (x) {
return dwv.precisionRound(x, precision);
};
}
/**
* Log the DICCOM seg segments ordered by frame position patients.
*
* @param {object} elements The DICOM seg elements.
*/
function logFramePosPats(elements) {
// PerFrameFunctionalGroupsSequence
const perFrame = elements['52009230'].value;
const perPos = {};
for (let i = 0; i < perFrame.length; ++i) {
// PlanePositionSequence
const posSq = perFrame[i]['00209113'].value;
// ImagePositionPatient
const pos = posSq[0]['00200032'].value;
if (typeof perPos[pos] === 'undefined') {
perPos[pos] = [];
}
// FrameContentSequence
const frameSq = perFrame[i]['00209111'].value;
// DimensionIndexValues
const dim = frameSq[0]['00209157'].value;
perPos[pos].push(dim);
}
console.log('DICOM SEG Segments', sortByPosPatKey(perPos));
}
/**
* Get an array reducer to reduce an array of tag keys taken from
* the input dataElements and return theses dataElements indexed by tag names.
*
* @param {Object<string, DataElement>} dataElements The meta data
* index by tag keys.
* @returns {Function} An array reducer.
*/
function getTagKeysReducer(dataElements) {
return function (accumulator, currentValue) {
// get the tag name
const tag = dwv.getTagFromKey(currentValue);
let tagName = tag.getNameFromDictionary();
if (typeof tagName === 'undefined') {
// add 'x' to list private at end
tagName = 'x' + tag.getKey();
}
const currentMeta = dataElements[currentValue];
// remove undefined properties
for (const property in currentMeta) {
if (typeof currentMeta[property] === 'undefined') {
delete currentMeta[property];
}
}
// recurse for sequences
if (currentMeta.vr === 'SQ') {
// valid for 1D array, not for merged data elements
for (let i = 0; i < currentMeta.value.length; ++i) {
const item = currentMeta.value[i];
currentMeta.value[i] = Object.keys(item).reduce(
getTagKeysReducer(item), {});
}
}
accumulator[tagName] = currentMeta;
return accumulator;
};
}
/**
* Get the meta data indexed by tag names instead of tag keys.
*
* @param {Object<string, DataElement>} metaData The meta data
* index by tag keys.
* @returns {Object<string, DataElement>} The meta data indexed by tag names.
*/
function getMetaDataWithNames(metaData) {
let meta = structuredClone(metaData);
if (typeof meta['00020010'] !== 'undefined') {
// replace tag key with tag name for dicom
meta = Object.keys(meta).reduce(getTagKeysReducer(meta), {});
}
return meta;
}
/**
* Setup test line.
*/
function setupTests() {
const renderTestButton = document.createElement('button');
renderTestButton.onclick = runRenderTest;
renderTestButton.appendChild(document.createTextNode('render test'));
const saveState = document.createElement('a');
saveState.appendChild(document.createTextNode('save state'));
saveState.href = '';
saveState.onclick = function () {
const blob = new Blob([_app.getJsonState()], {type: 'application/json'});
saveState.href = window.URL.createObjectURL(blob);
};
saveState.download = 'state.json';
const testsDiv = document.getElementById('tests');
testsDiv.appendChild(renderTestButton);
testsDiv.appendChild(saveState);
}
/**
* Get basic stats for an array.
*
* @param {Array} array Input array.
* @returns {object} Min, max, mean and standard deviation.
*/
function getBasicStats(array) {
let min = array[0];
let max = min;
let sum = 0;
let sumSqr = 0;
let val = 0;
const length = array.length;
for (let i = 0; i < length; ++i) {
val = array[i];
if (val < min) {
min = val;
} else if (val > max) {
max = val;
}
sum += val;
sumSqr += val * val;
}
const mean = sum / length;
// see http://en.wikipedia.org/wiki/Algorithms_for_calculating_variance
const variance = sumSqr / length - mean * mean;
const stdDev = Math.sqrt(variance);
return {
min: min,
max: max,
mean: mean,
stdDev: stdDev
};
}
/**
* Run render tests.
*/
function runRenderTest() {
const numberOfRun = 20;
// default to first layer group
_app.setActiveLayerGroup(1);
const vl = _app.getActiveLayerGroup().getActiveViewLayer();
if (typeof vl === 'undefined') {
return;
}
const vc = vl.getViewController();
const runner = function () {
vc.incrementScrollIndex();
};
let startTime;
const timings = [];
const onRenderStart = function (/*event*/) {
startTime = performance.now();
};
const onRenderEnd = function (/*event*/) {
const endTime = performance.now();
timings.push(endTime - startTime);
if (timings.length < numberOfRun) {
setTimeout(() => {
runner();
}, 100);
} else {
console.log('Stats:', getBasicStats(timings));
// clean up
_app.removeEventListener('renderstart', onRenderStart);
_app.removeEventListener('renderend', onRenderEnd);
}
};
// setup
_app.addEventListener('renderstart', onRenderStart);
_app.addEventListener('renderend', onRenderEnd);
// start
runner();
}
/**
* Setup about line.
*/
function setupAbout() {
const testsDiv = document.getElementById('about');
const link = document.createElement('a');
link.href = 'https://github.com/ivmartel/dwv';
link.appendChild(document.createTextNode('dwv'));
const text = document.createTextNode(
' v' + dwv.getDwvVersion() +
' on ' + navigator.userAgent);
testsDiv.appendChild(link);
testsDiv.appendChild(text);
}