tests_pacs_viewer.ui.annot.js
import {DrawController} from '../../src/app/drawController.js';
import {
AnnotationGroupFactory
} from '../../src/image/annotationGroupFactory.js';
import {
getUID,
DicomWriter
} from '../../src/dicom/dicomWriter.js';
import {i18n} from '../../src/utils/i18n.js';
import {
getIconElement,
getButton,
setButtonPressed,
isButtonPressed
} from './viewer.ui.icons.js';
// doc imports
/* eslint-disable no-unused-vars */
import {App} from '../../src/app/application.js';
import {Annotation} from '../../src/image/annotation.js';
import {AnnotationGroup} from '../../src/image/annotationGroup.js';
/* eslint-enable no-unused-vars */
/**
* Get the annotation group divId.
*
* @param {string} dataId The data ID.
* @returns {string} The divId.
*/
function getAnnotationGroupDivId(dataId) {
return 'annotationgroup' + dataId;
}
/**
* Get the annotation divId.
*
* @param {Annotation} annotation The annotation.
* @param {string} dataId The data ID.
* @returns {string} The divId.
*/
function getAnnotationDivId(annotation, dataId) {
const prefix = getAnnotationGroupDivId(dataId);
const suffix = 'annotation' + annotation.trackingUid;
return prefix + '-' + suffix;
}
/**
* Split a divId to get dataId and annotationId.
*
* @param {string} divId The divId.
* @returns {object} The data and annotation ID.
*/
function splitAnnotationDivId(divId) {
const split = divId.split('-');
const prefixStrSize = 'annotationgroup'.length;
const suffixStrSize = 'annotation'.length;
return {
dataId: split[0].substring(prefixStrSize),
annotationId: split[1].substring(suffixStrSize)
};
}
/**
* Annotation UI.
*/
export class AnnotationUI {
/**
* The associated application.
*
* @type {App}
*/
#app;
/**
* The root document.
*
* @type {Document}
*/
#rootDoc = document;
/**
* @param {App} app The associated application.
* @param {Document} [rootDoc] Optional root document,
* defaults to `window.document`.
*/
constructor(app, rootDoc) {
this.#app = app;
if (typeof rootDoc !== 'undefined') {
this.#rootDoc = rootDoc;
}
}
/**
* Bind app to ui.
*/
registerListeners() {
this.#app.addEventListener('dataadd', this.#onDataAdd);
this.#app.addEventListener('drawlayeradd', this.#onDrawLayerAdd);
this.#app.addEventListener('annotationadd', this.#onAnnotationAdd);
this.#app.addEventListener('annotationupdate', this.#onAnnotationUpdate);
this.#app.addEventListener('annotationremove', this.#onAnnotationRemove);
};
/**
* Setup the container div.
*/
#setupContainerDiv() {
// fieldset
const legend = document.createElement('legend');
legend.appendChild(document.createTextNode('Annotation Groups'));
const fieldset = document.createElement('fieldset');
fieldset.id = 'annotationgroups-fieldset';
fieldset.appendChild(legend);
// main div
const line = document.createElement('div');
line.id = 'annotationgroups-line';
line.className = 'line';
line.appendChild(fieldset);
// insert
const detailsEl = this.#rootDoc.getElementById('layersdetails');
detailsEl.parentElement.insertBefore(line, detailsEl);
}
/**
* Get the container div.
*
* @returns {HTMLDivElement} The element.
*/
#getContainerDiv() {
return this.#rootDoc.getElementById('annotationgroups-fieldset');
}
/**
* Setup the html for the annotation list.
*/
#setupHtml() {
// add annotation group button
const addItem = document.createElement('li');
addItem.id = 'addannotationgroupitem';
const addAnnotationGroupButton = document.createElement('button');
addAnnotationGroupButton.appendChild(
document.createTextNode('Add annotation group'));
addAnnotationGroupButton.onclick = () => {
const divId = 'layerGroup0';
const layerGroup = this.#app.getLayerGroupByDivId(divId);
// add annotation group
const viewLayer = layerGroup.getActiveViewLayer();
if (typeof viewLayer === 'undefined') {
console.warn(
'No active view layer, please select one in the data table'
);
return;
}
const refDataId = viewLayer.getDataId();
const data = this.#app.createAnnotationData(refDataId);
// render (will create draw layer)
this.#app.addAndRenderAnnotationData(data, divId, refDataId);
// item is added to the UI by the 'dataadd' listener
};
addItem.appendChild(addAnnotationGroupButton);
// annotation list
const annotList = document.createElement('ul');
annotList.id = 'annotationgroup-list';
annotList.className = 'data-list';
annotList.appendChild(addItem);
// setup and append
this.#setupContainerDiv();
this.#getContainerDiv().appendChild(annotList);
}
/**
* Get the annotation html.
*
* @param {Annotation} annotation The annotation.
* @param {string} dataId The annotation group dataId.
* @returns {HTMLLIElement} The HTMl element.
*/
#getAnnotationHtml(annotation, dataId) {
const annotationDivId = getAnnotationDivId(annotation, dataId);
const infoButton = getButton('Info');
const ibIdPrefix = 'ib-';
infoButton.id = ibIdPrefix + annotationDivId;
infoButton.title = 'Information';
infoButton.onclick = (event) => {
// do not propagate to parent that triggers goto
event.stopPropagation();
const target = event.target;
// get annotatio
const indices =
splitAnnotationDivId(target.id.substring(vbIdPrefix.length));
const dataId = indices.dataId;
const annotationId = indices.annotationId;
const annotationGroup = this.#app.getData(dataId).annotationGroup;
const annotation = annotationGroup.find(annotationId);
let qStr = 'Quantification:\n';
if (typeof annotation.quantification !== 'undefined') {
const keys = Object.keys(annotation.quantification);
for (const key of keys) {
const quant = annotation.quantification[key];
qStr += '- ' + key + ': ' +
quant.value.toPrecision(4) +
i18n.t(quant.unit);
qStr += '\n';
}
} else {
qStr = 'No quantification.';
}
alert(qStr);
};
const inputColour = document.createElement('input');
inputColour.type = 'color';
inputColour.title = 'Change annotation colour';
const inputColourPrefix = 'cb-';
inputColour.id = inputColourPrefix + annotationDivId;
inputColour.value = annotation.colour;
inputColour.onclick = (event) => {
// do not propagate to parent that triggers goto
event.stopPropagation();
};
inputColour.onchange = (event) => {
const target = event.target;
const newColour = target.value;
// get annotation
const indices =
splitAnnotationDivId(target.id.substring(inputColourPrefix.length));
const dataId = indices.dataId;
const annotationId = indices.annotationId;
const annotationGroup = this.#app.getData(dataId).annotationGroup;
const annotation = annotationGroup.find(annotationId);
// update
if (newColour !== annotation.colour) {
const drawController = new DrawController(annotationGroup);
drawController.updateAnnotationWithCommand(
annotationId,
{colour: annotation.colour},
{colour: newColour},
this.#app.addToUndoStack
);
}
};
const viewButton = getButton('View');
setButtonPressed(viewButton, false);
const vbIdPrefix = 'vb-';
viewButton.id = vbIdPrefix + annotationDivId;
viewButton.title = 'Show/hide annotation';
viewButton.onclick = (event) => {
// do not propagate to parent (triggers goto)
event.stopPropagation();
const target = event.target;
// get annotatio
const indices =
splitAnnotationDivId(target.id.substring(vbIdPrefix.length));
const dataId = indices.dataId;
const annotationId = indices.annotationId;
const drawLayers = this.#app.getDrawLayersByDataId(dataId);
// toggle hidden
if (isButtonPressed(target)) {
setButtonPressed(target, false);
for (const layer of drawLayers) {
layer.setAnnotationVisibility(annotationId, true);
}
} else {
setButtonPressed(target, true);
for (const layer of drawLayers) {
layer.setAnnotationVisibility(annotationId, false);
}
}
};
const deleteButton = getButton('Delete');
const dbIdPrefix = 'db-';
deleteButton.id = dbIdPrefix + annotationDivId;
deleteButton.title = 'Delete annotation';
deleteButton.onclick = (event) => {
// do not propagate to parent (triggers goto)
event.stopPropagation();
const target = event.target;
// get segment and mask
const indices =
splitAnnotationDivId(target.id.substring(dbIdPrefix.length));
const dataId = indices.dataId;
const annotationId = indices.annotationId;
// delete if possible
const drawController = new DrawController(
this.#app.getData(dataId).annotationGroup);
// TODO reposition div at same position after delete undo?
drawController.removeAnnotationWithCommand(
annotationId,
this.#app.addToUndoStack
);
};
// disable/enable buttons if group is editable or not
const annotationGroup = this.#app.getData(dataId).annotationGroup;
annotationGroup.addEventListener(
'annotationgroupeditablechange', function (event) {
const disabled = !event.data;
inputColour.disabled = disabled;
deleteButton.disabled = disabled;
}
);
// content
const contentDiv = document.createElement('div');
contentDiv.className = 'data-item-list-item-content';
let factoryName = 'unknown';
if (typeof annotation.getFactory() !== 'undefined') {
factoryName = annotation.getFactory().getName();
}
contentDiv.appendChild(getIconElement(factoryName));
contentDiv.appendChild(document.createTextNode(
' ' + annotation.trackingId));
// actions
const actionsDiv = document.createElement('div');
actionsDiv.className = 'data-item-list-item-actions';
actionsDiv.appendChild(infoButton);
actionsDiv.appendChild(inputColour);
actionsDiv.appendChild(viewButton);
actionsDiv.appendChild(deleteButton);
// list item
const item = document.createElement('li');
item.id = annotationDivId;
item.className = 'data-item-list-item';
item.appendChild(contentDiv);
item.appendChild(actionsDiv);
item.title = 'Go to annotation';
// click on li to go to annotation
item.addEventListener('click', (event) => {
const target = event.currentTarget;
// remove selected class from other rows
const mainlist = this.#rootDoc.getElementById('annotationgroup-list');
const items = mainlist.querySelectorAll('.data-item-list-item');
items.forEach(item => item.classList.remove('selected'));
// mark this row as selected
target.classList.add('selected');
// get annotation
const indices = splitAnnotationDivId(target.id);
const dataId = indices.dataId;
const annotationId = indices.annotationId;
const annotationGroup = this.#app.getData(dataId).annotationGroup;
const annotation = annotationGroup.find(annotationId);
const annotCentroid = annotation.getCentroid();
if (typeof annotCentroid !== 'undefined') {
const drawLayers = this.#app.getDrawLayersByDataId(dataId);
for (const layer of drawLayers) {
layer.setCurrentPosition(annotCentroid);
}
} else {
console.log('No centroid for annotation');
}
});
return item;
}
/**
* Get an annotation group html.
*
* @param {AnnotationGroup} annotationGroup The annotation group.
* @param {string} dataId The annotation group dataId.
* @returns {HTMLIement} The annotation list element.
*/
#getAnnotationGroupHtml(annotationGroup, dataId) {
// name
const nameDiv = document.createElement('span');
nameDiv.id = getAnnotationGroupDivId(dataId) + '-name';
nameDiv.className = 'data-item-name';
nameDiv.appendChild(document.createTextNode('group #' + dataId));
// lock button
const lockButton = getButton('Lock');
setButtonPressed(lockButton, false);
lockButton.id = 'lockb-' + getAnnotationGroupDivId(dataId);
lockButton.onclick = function (event) {
const target = event.target;
// toggle hidden
if (isButtonPressed(target)) {
setButtonPressed(target, false);
if (typeof annotationGroup !== 'undefined') {
annotationGroup.setEditable(true);
}
} else {
setButtonPressed(target, true);
if (typeof annotationGroup !== 'undefined') {
annotationGroup.setEditable(false);
}
}
};
// save button
const saveButton = getButton('Save');
saveButton.title = 'Save annnotation group';
saveButton.onclick = function () {
const factory = new AnnotationGroupFactory();
const sopUID = getUID('SOPInstanceUID');
const extraTags = {
MediaStorageSOPInstanceUID: sopUID,
SOPInstanceUID: sopUID,
InstanceNumber: 123,
SeriesInstanceUID: getUID('SeriesInstanceUID'),
SeriesNumber: 123,
SeriesDescription: 'Annnotation made with dwv',
};
const dicomElements = factory.toDicom(annotationGroup, extraTags);
// write
const writer = new DicomWriter();
let dicomBuffer = null;
try {
dicomBuffer = writer.getBuffer(dicomElements);
} catch (error) {
console.error(error);
alert(error.message);
}
const blob = new Blob([dicomBuffer], {type: 'application/dicom'});
saveButton.href = window.URL.createObjectURL(blob);
// temporary link to download
const element = document.createElement('a');
element.href = window.URL.createObjectURL(blob);
element.download = 'dicom-sr-' + dataId + '.dcm';
// trigger download
element.click();
URL.revokeObjectURL(element.href);
};
// hide button
const hideLabelsButton = getButton('Label');
setButtonPressed(hideLabelsButton, false);
hideLabelsButton.id = 'b-hidelabels';
hideLabelsButton.title = 'Show/hide annotation labels';
hideLabelsButton.onclick = (event) => {
const target = event.target;
const drawLayer = this.#app.getDrawLayersByDataId(dataId)[0];
if (typeof drawLayer === 'undefined') {
console.warn('Cannot find draw layer with id ' + dataId);
}
if (isButtonPressed(target)) {
setButtonPressed(target, false);
drawLayer.setLabelsVisibility(true);
} else {
setButtonPressed(target, true);
drawLayer.setLabelsVisibility(false);
}
};
// actions
const actionGroupDiv = document.createElement('div');
actionGroupDiv.id = getAnnotationGroupDivId(dataId) + '-actions';
actionGroupDiv.className = 'data-item-actions';
actionGroupDiv.appendChild(lockButton);
actionGroupDiv.appendChild(saveButton);
actionGroupDiv.appendChild(hideLabelsButton);
// annotation list
const listDiv = document.createElement('ul');
listDiv.id = getAnnotationGroupDivId(dataId) + '-list';
listDiv.className = 'data-item-list';
for (const annotation of annotationGroup.getList()) {
listDiv.appendChild(this.#getAnnotationHtml(annotation, dataId));
}
// data-item-header
const headerDiv = document.createElement('div');
headerDiv.id = getAnnotationGroupDivId(dataId) + '-header';
headerDiv.className = 'data-item-header';
headerDiv.appendChild(nameDiv);
headerDiv.appendChild(actionGroupDiv);
// data-item-content
const contentDiv = document.createElement('div');
contentDiv.id = getAnnotationGroupDivId(dataId) + '-content';
contentDiv.className = 'data-item-content';
contentDiv.appendChild(listDiv);
// data-item
const item = document.createElement('li');
item.id = getAnnotationGroupDivId(dataId);
item.className = 'data-item';
item.appendChild(headerDiv);
item.appendChild(contentDiv);
return item;
};
/**
* Handle 'dataadd' event.
*
* @param {object} event The event.
*/
#onDataAdd = (event) => {
const data = this.#app.getData(event.dataid);
const ag = data.annotationGroup;
if (typeof ag !== 'undefined') {
// setup html if needed
if (!this.#rootDoc.getElementById('annotationgroup-list')) {
this.#setupHtml();
}
// annotation group as html
const item = this.#getAnnotationGroupHtml(ag, event.dataid);
// add annotation group item
const addItem = this.#rootDoc.getElementById('addannotationgroupitem');
// remove and add after to make it last item
addItem.remove();
// update list
const annotList = this.#rootDoc.getElementById('annotationgroup-list');
annotList.appendChild(item);
annotList.appendChild(addItem);
}
};
/**
* Handle 'drawlayeradd' event.
*
* @param {object} event The event.
*/
#onDrawLayerAdd = (event) => {
const dataId = event.dataid;
const annotationGroup = this.#app.getData(dataId).annotationGroup;
// strike through non viewable annotations
for (const annotation of annotationGroup.getList()) {
let textDecoration = '';
if (!annotation.canView()) {
textDecoration = 'line-through';
}
const annotationDivId = getAnnotationDivId(annotation, dataId);
const item = this.#rootDoc.getElementById(annotationDivId);
if (item) {
item.style['text-decoration-line'] = textDecoration;
}
}
};
/**
* Handle 'annotationadd' event.
*
* @param {object} event The event.
*/
#onAnnotationAdd = (event) => {
const annotation = event.data;
const dataId = event.dataid;
// add item to list
const listDivId = getAnnotationGroupDivId(dataId) + '-list';
const listDiv = this.#rootDoc.getElementById(listDivId);
listDiv.appendChild(this.#getAnnotationHtml(annotation, dataId));
};
/**
* Handle 'annotationupdate' event.
*
* @param {object} event The event.
*/
#onAnnotationUpdate = (event) => {
const annotation = event.data;
const dataId = event.dataid;
const keys = event.keys;
if (typeof keys !== 'undefined') {
const annotationDivId = getAnnotationDivId(annotation, dataId);
// update colour input
if (keys.includes('colour')) {
const inputColour = this.#rootDoc.getElementById(
'cb-' + annotationDivId);
inputColour.value = annotation.colour;
}
}
};
/**
* Handle 'annotationremove' event.
*
* @param {object} event The event.
*/
#onAnnotationRemove = (event) => {
const annotation = event.data;
const dataId = event.dataid;
// remove annotation from list
const annotationDivId = getAnnotationDivId(annotation, dataId);
const item = this.#rootDoc.getElementById(annotationDivId);
item.remove();
};
}; // AnnotationUI