import {DicomWriter} from '../../src/dicom/dicomWriter.js';
import {
rgbToHex,
hexToRgb,
} from '../../src/utils/colour.js';
import {logger} from '../../src/utils/logger.js';
import {i18n} from '../../src/utils/i18n.js';
import {getSegmentationCode} from '../../src/dicom/dicomCode.js';
import {MaskFactory} from '../../src/image/maskFactory.js';
import {MaskSegmentHelper} from '../../src/image/maskSegmentHelper.js';
import {MaskSegmentViewHelper} from '../../src/image/maskSegmentViewHelper.js';
import {
ChangeSegmentColourCommand
} from '../../src/image/changeSegmentColourCommand.js';
import {
DeleteSegmentCommand
} from '../../src/image/deleteSegmentCommand.js';
import {
getHtmlId,
getRootFromHtmlId
} from './viewer.ui.js';
// global vars
const _colours = [
{r: 255, g: 0, b: 0},
{r: 0, g: 255, b: 0},
{r: 0, g: 0, b: 255},
{r: 0, g: 255, b: 255},
{r: 255, g: 0, b: 255},
{r: 255, g: 255, b: 0},
{r: 255, g: 255, b: 255},
{r: 255, g: 128, b: 128},
{r: 128, g: 255, b: 128},
{r: 128, g: 128, b: 255},
{r: 128, g: 255, b: 255},
{r: 255, g: 128, b: 255},
{r: 255, g: 255, b: 128},
{r: 128, g: 128, b: 128},
{r: 255, g: 64, b: 64},
{r: 64, g: 255, b: 64},
{r: 64, g: 64, b: 255},
{r: 64, g: 255, b: 255},
{r: 255, g: 64, b: 255},
{r: 255, g: 255, b: 64},
{r: 64, g: 64, b: 64},
];
// colour array to pick from
let _coloursPick = _colours.slice();
// segmentation
const _segmentations = [];
/**
* Get a segment from a segment list.
*
* @param {number} segmentNumber The segment number.
* @param {object[]} segments The list to search.
* @returns {object|undefined} The found segment.
*/
function getSegment(segmentNumber, segments) {
return segments.find(function (item) {
return item.number === segmentNumber;
});
}
/**
* Get the next available colour from the colour list.
*
* @returns {object} The colour as {r,g,b}.
*/
function nextColour() {
// recreate if empty
if (_coloursPick.length === 0) {
console.log('Regenerating colours...');
_coloursPick = _colours.slice();
}
// pick first in list
const colour = _coloursPick[0];
// remove picked
_coloursPick.splice(0, 1);
// return first
return colour;
}
/**
* Get a new segment.
*
* @param {number} number The segment number.
* @returns {object} The new segment.
*/
function getNewSegment(number) {
return {
number,
algorithmType: 'MANUAL',
algorithmName: undefined,
label: 's' + number,
displayRGBValue: nextColour(),
displayValue: undefined,
propertyCategoryCode: getSegmentationCode(),
propertyTypeCode: getSegmentationCode(),
trackingId: undefined,
trackingUid: undefined
};
}
/**
* HTML element id prefixes.
*/
const prefixes = {
segmentation: 'segmentation',
segment: 'segment',
span: 'span-',
select: 'select-',
colour: 'colour-',
view: 'view-',
delete: 'delete-',
addSegment: 'add-segment-',
selectEraser: 'select-eraser-',
save: 'save-',
volumes: 'span-volumes-',
goto: 'gotob-'
};
/**
* Get the HTML id of a segmentation.
*
* @param {number} segmentationIndex The segmentation index.
* @returns {string} The segmentation HTML id.
*/
function getSegmentationHtmlId(segmentationIndex) {
return getHtmlId(prefixes.segmentation, segmentationIndex);
}
/**
* Get a segmentation index from an HTML id.
*
* @param {string} segmentationName The segmentation HTML id.
* @returns {number} The segmentation index.
*/
function splitSegmentationHtmlId(segmentationName) {
const indexStr = getRootFromHtmlId(
prefixes.segmentation, segmentationName);
return parseInt(indexStr, 10);
}
/**
* Get the HTML id of a segment.
*
* @param {number} segmentNumber The segment number.
* @param {number} segmentationIndex The segmentation index.
* @returns {string} The segment HTML id.
*/
function getSegmentHtmlId(segmentNumber, segmentationIndex) {
const segmentName = getHtmlId(prefixes.segment, segmentNumber);
const segmentationName = getSegmentationHtmlId(segmentationIndex);
return segmentName + '-' + segmentationName;
}
/**
* Get a segment index and number from an HTML id.
*
* @param {string} segmentId The segment id.
* @returns {object} The segment index and number.
*/
function splitSegmentHtmlId(segmentId) {
const split = segmentId.split('-');
const numberStr = getRootFromHtmlId(prefixes.segment, split[0]);
return {
segmentNumber: parseInt(numberStr, 10),
segmentationIndex: splitSegmentationHtmlId(split[1])
};
}
/**
* Segmentation UI.
*/
export class SegmentationUI {
#watching = {};
#app;
/**
* @param {object} app The associated application.
*/
constructor(app) {
this.#app = app;
}
/**
* Watch a data for changes.
*
* @param {string} dataId The data ID.
*/
#watchData(dataId) {
if (!this.#watching[dataId]) {
const maskData = this.#app.getData(dataId);
if (!maskData) {
throw new Error(
'No data to watch for dataId: ' + dataId
);
}
const image = maskData.image;
// Watch for labels change
image.addEventListener(
'labelschanged',
(event) => {
const segmentation =
_segmentations.find(
(seg) => {
return seg.dataId === dataId;
}
);
if (typeof segmentation !== 'undefined') {
segmentation.labels = event.labels;
this.#updateLabelsSpan(segmentation);
}
}
);
image.recalculateLabels();
this.#watching[dataId] = true;
} else {
console.log('Already watching data', dataId);
}
}
/**
* Bind app to ui.
*/
registerListeners() {
this.#app.addEventListener('dataadd', this.#onDataAdd);
};
/**
* Add a segment HTML to the main HTML.
*
* @param {object} segmentation The segmentation.
*/
#addSegmentationHtml(segmentation) {
// segmentation as html
const item =
this.#getSegmentationHtml(segmentation, _segmentations.length - 1);
// add segmentation item
const addItem = document.getElementById('addsegmentationitem');
// remove and add after to make it last item
addItem.remove();
// update list
const segList = document.getElementById('segmentation-list');
segList.appendChild(item);
segList.appendChild(addItem);
}
/**
* Handle a dataadd event.
*
* @param {object} event The dataadd event.
*/
#onDataAdd = (event) => {
const dataId = event.dataid;
const maskImage = this.#app.getData(dataId).image;
if (typeof maskImage !== 'undefined' &&
maskImage.getMeta().Modality === 'SEG') {
// setup html if needed
if (!document.getElementById('segmentation-list')) {
this.#setupHtml();
}
const segHelper = new MaskSegmentHelper(maskImage);
if (segHelper.getNumberOfSegments() === 0) {
// manually created segmentation with no segments
const selectSegmentCheckedId = this.#getSelectSegmentCheckedId();
if (typeof selectSegmentCheckedId === 'undefined') {
// default segment created at first brush
const segmentNumber = 1;
const segment = getNewSegment(segmentNumber);
segHelper.addSegment(segment);
// default segmentation
const segmentation = {
dataId: dataId,
labels: [],
hasNewSegments: false,
segments: [segment],
selectedSegmentNumber: segmentNumber,
viewHelper: new MaskSegmentViewHelper()
};
this.#watchData(dataId);
// add to list
_segmentations.push(segmentation);
// add to html
this.#addSegmentationHtml(segmentation);
} else {
const indices = splitSegmentHtmlId(
getRootFromHtmlId(prefixes.select, selectSegmentCheckedId));
const segmentation = _segmentations[indices.segmentationIndex];
// segmentation created with add segmentation
if (typeof segmentation.dataId === 'undefined') {
segmentation.dataId = dataId;
this.#watchData(dataId);
for (const segment of segmentation.segments) {
segHelper.addSegment(segment);
}
}
segmentation.hasNewSegments = false;
}
} else {
// segmentation from loaded file, pass segments to ui
const imgMeta = maskImage.getMeta();
if (typeof imgMeta !== 'undefined') {
// loaded segmentation
const segmentation = {
dataId: dataId,
labels: [],
hasNewSegments: true,
segments: imgMeta.custom.segments,
viewHelper: new MaskSegmentViewHelper()
};
this.#watchData(dataId);
// add to list
_segmentations.push(segmentation);
// add to html
this.#addSegmentationHtml(segmentation);
// remove colour from colour pick
for (const segment of imgMeta.custom.segments) {
const index = _coloursPick.findIndex((item) =>
item.r === segment.displayRGBValue.r &&
item.g === segment.displayRGBValue.g &&
item.b === segment.displayRGBValue.b);
if (index !== -1) {
_coloursPick.splice(index, 1);
}
}
}
}
}
};
/**
* Setup the html for the segmentation list.
*/
#setupHtml() {
// segmentation list
const segList = document.createElement('ul');
segList.id = 'segmentation-list';
// loop on segmentations
for (let i = 0; i < _segmentations.length; ++i) {
const segmentationItem = this.#getSegmentationHtml(_segmentations[i], i);
segList.appendChild(segmentationItem);
}
// extra item for add segmentation button
const addItem = document.createElement('li');
addItem.id = 'addsegmentationitem';
const addSegmentationButton = document.createElement('button');
addSegmentationButton.appendChild(
document.createTextNode('Add segmentation'));
addSegmentationButton.onclick = (/*event*/) => {
// new segmentation
const segmentation = {
dataId: undefined,
labels: [],
hasNewSegments: true,
segments: [getNewSegment(1)],
viewHelper: new MaskSegmentViewHelper()
};
// add to list
_segmentations.push(segmentation);
// add to html
this.#addSegmentationHtml(segmentation);
};
addItem.appendChild(addSegmentationButton);
segList.appendChild(addItem);
// fieldset
const legend = document.createElement('legend');
legend.appendChild(document.createTextNode('Segmentations'));
const fieldset = document.createElement('fieldset');
fieldset.appendChild(legend);
fieldset.appendChild(segList);
// main div
const line = document.createElement('div');
line.id = 'segmentations-line';
line.className = 'line';
line.appendChild(fieldset);
// insert
const detailsEl = document.getElementById('layersdetails');
detailsEl.parentElement.insertBefore(line, detailsEl);
}
/**
* Select a segment in the brush tool.
*
* @param {number} segmentNumber The segment number.
* @param {object} segmentation The segmentation.
*/
#appSelectSegment(segmentNumber, segmentation) {
segmentation.selectedSegmentNumber = segmentNumber;
// add segment if not present
const data = this.#app.getData(segmentation.dataId);
if (typeof data !== 'undefined') {
const maskImage = data.image;
const segHelper = new MaskSegmentHelper(maskImage);
// add segment to mask
if (!segHelper.hasSegment(segmentNumber)) {
console.log('Add segment', segmentNumber);
segHelper.addSegment(getSegment(
segmentNumber, segmentation.segments
));
}
}
// app features
const features = {
brushMode: 'add',
selectedSegmentNumber: segmentNumber,
maskDataId: undefined,
createMask: false
};
if (typeof segmentation.dataId !== 'undefined') {
features.maskDataId = segmentation.dataId;
} else {
features.createMask = true;
}
console.log('set tool features [add]', features);
this.#app.setToolFeatures(features);
}
/**
* Select the erase in the brush tool.
*
* @param {object} segmentation The segmentation.
*/
#appSelectEraser(segmentation) {
// app features
const features = {
brushMode: 'del',
maskDataId: undefined
};
if (typeof segmentation.dataId !== 'undefined') {
features.maskDataId = segmentation.dataId;
}
console.log('set tool features [del]', features);
this.#app.setToolFeatures(features);
}
/**
* Handle a segment select from UI.
*
* @param {Event} event HTML event.
*/
#onSegmentSelect = (event) => {
const target = event.target;
// get segment
const indices = splitSegmentHtmlId(
getRootFromHtmlId(prefixes.select, target.id));
const segmentation = _segmentations[indices.segmentationIndex];
// select it
this.#appSelectSegment(indices.segmentNumber, segmentation);
};
/**
* Handle a segment colour change from UI.
*
* @param {Event} event HTML event.
*/
#onSegmentColourChange = (event) => {
const target = event.target;
const newHexColour = target.value;
// get segment
const indices = splitSegmentHtmlId(
getRootFromHtmlId(prefixes.colour, target.id));
const segmentation = _segmentations[indices.segmentationIndex];
const segment = getSegment(indices.segmentNumber, segmentation.segments);
const segmentHexColour = rgbToHex(segment.displayRGBValue);
if (newHexColour !== segmentHexColour) {
// update colours
const newRgbColour = hexToRgb(newHexColour);
// get segment and mask
const maskData = this.#app.getData(segmentation.dataId);
// change if possible
if (typeof maskData !== 'undefined') {
// create change colour command
const previousColour = segment.displayRGBValue;
const chgCmd = new ChangeSegmentColourCommand(
maskData.image, segment, newRgbColour);
chgCmd.onExecute = function (/*event*/) {
// not needed the first time but on undo/redo
target.value = newHexColour;
};
chgCmd.onUndo = function () {
// not needed the first time but on undo/redo
target.value = rgbToHex(previousColour);
};
// execute command
if (chgCmd.isValid()) {
chgCmd.execute();
this.#app.addToUndoStack(chgCmd);
}
}
// update segment
segment.displayRGBValue = newRgbColour;
// pass updated color to brush
this.#appSelectSegment(indices.segmentNumber, segmentation);
}
};
/**
* Handle a goto segment.
*
* @param {MouseEvent} event HTML event.
*/
#onGotoSegment = (event) => {
const target = event.target;
// get segment
const indices = splitSegmentHtmlId(
getRootFromHtmlId(prefixes.goto, target.id));
const segmentation = _segmentations[indices.segmentationIndex];
const segment = getSegment(indices.segmentNumber, segmentation.segments);
// Find the first label for this segment
const label =
segmentation.labels.find((item) => {
return item.id === segment.number;
});
if (typeof label !== 'undefined') {
const dataId = segmentation.dataId;
const drawLayers = this.#app.getViewLayersByDataId(dataId);
for (const layer of drawLayers) {
layer.setCurrentPosition(label.centroid);
}
} else {
console.log('No label for this segment');
}
};
/**
* Handle a segment view change from UI.
*
* @param {MouseEvent} event HTML event.
*/
#onSegmentViewChange = (event) => {
const target = event.target;
// get segment
const indices = splitSegmentHtmlId(
getRootFromHtmlId(prefixes.view, target.id));
const segmentation = _segmentations[indices.segmentationIndex];
const segment = getSegment(indices.segmentNumber, segmentation.segments);
// toggle hidden
const segViewHelper = segmentation.viewHelper;
const isPressed = target.style.borderStyle === 'inset';
if (isPressed) {
target.style.borderStyle = 'outset';
segViewHelper.removeFromHidden(segment.number);
} else {
target.style.borderStyle = 'inset';
segViewHelper.addToHidden(segment.number);
}
// apply hidden
const vls = this.#app.getViewLayersByDataId(segmentation.dataId);
if (vls.length === 0) {
console.warn('No layers to show/hide seg');
}
for (const vl of vls) {
const vc = vl.getViewController();
vc.setViewAlphaFunction(segViewHelper.getAlphaFunc());
}
};
/**
* Handle a segment delete from UI.
*
* @param {MouseEvent} event HTML event.
*/
#onSegmentDelete = (event) => {
const target = event.target;
// get segment
const indices = splitSegmentHtmlId(
getRootFromHtmlId(prefixes.delete, target.id));
const segmentation = _segmentations[indices.segmentationIndex];
const segmentId = getSegmentHtmlId(
indices.segmentNumber, indices.segmentationIndex);
// get segment divs
const segmentSpan = document.getElementById(
getHtmlId(prefixes.span, segmentId)
);
if (!segmentSpan) {
throw new Error('No delete span');
}
const spanParent = segmentSpan.parentNode;
if (!spanParent) {
throw new Error('No delete span parent');
}
const spanNext = segmentSpan.nextSibling;
// get mask
const data = this.#app.getData(segmentation.dataId);
// delete if possible
if (typeof data !== 'undefined') {
const segment =
getSegment(indices.segmentNumber, segmentation.segments);
// create delete command
const delCmd = new DeleteSegmentCommand(data.image, segment);
delCmd.onExecute = function () {
segmentSpan.remove();
if (segmentation.viewHelper.isHidden(segment.number)) {
segmentation.viewHelper.removeFromHidden(segment);
}
};
delCmd.onUndo = function () {
spanParent.insertBefore(segmentSpan, spanNext);
};
// execute command
if (delCmd.isValid()) {
delCmd.execute();
this.#app.addToUndoStack(delCmd);
}
} else {
segmentSpan.remove();
}
this.#watchData(segmentation.dataId);
// select first segment
const spanChildren = spanParent.childNodes;
for (const spanNode of spanChildren) {
if (spanNode.nodeName === 'SPAN') {
const spanNodeChildren = spanNode.childNodes;
for (const node of spanNodeChildren) {
if (node.nodeName === 'INPUT') {
const input = node;
input.checked = true;
break;
}
}
break;
}
}
};
/**
* Get the id of the select segment checked input.
*
* @returns {string} The input id.
*/
#getSelectSegmentCheckedId() {
let id;
const selectInputs = document.querySelectorAll(
'input[type=\'radio\'][name=\'select-segment\']'
);
for (const input of selectInputs) {
if (input.checked) {
id = input.id;
break;
}
}
return id;
}
/**
* Get the HTML span element for a segment.
*
* @param {object} segment The segment.
* @param {number} segmentationIndex The segmentation index.
* @returns {HTMLSpanElement} THe HTML element.
*/
#getSegmentHtml(segment, segmentationIndex) {
const segmentId = getSegmentHtmlId(segment.number, segmentationIndex);
// segment select
const selectInput = document.createElement('input');
selectInput.type = 'radio';
selectInput.name = 'select-segment';
selectInput.id = getHtmlId(prefixes.select, segmentId);
selectInput.title = segmentId;
selectInput.onchange = this.#onSegmentSelect;
if (segment.number === 1) {
selectInput.checked = true;
this.#appSelectSegment(segment.number, _segmentations[segmentationIndex]);
}
const selectLabel = document.createElement('label');
selectLabel.htmlFor = selectInput.id;
selectLabel.title = selectInput.title;
selectLabel.appendChild(document.createTextNode(segment.label));
// volumes display
const volumesSpan = document.createElement('span');
volumesSpan.id = getHtmlId(prefixes.volumes, segmentId);
volumesSpan.innerText = this.#getLabelsString(segment, segmentationIndex);
// segment colour
const colourInput = document.createElement('input');
colourInput.type = 'color';
colourInput.title = 'Change segment colour';
colourInput.id = getHtmlId(prefixes.colour, segmentId);
colourInput.value = rgbToHex(segment.displayRGBValue);
colourInput.onchange = this.#onSegmentColourChange;
// segment view
const viewButton = document.createElement('button');
viewButton.style.borderStyle = 'outset';
viewButton.id = getHtmlId(prefixes.view, segmentId);
viewButton.title = 'Show/hide segment';
viewButton.appendChild(document.createTextNode('\u{1F441}\u{FE0F}'));
viewButton.onclick = this.#onSegmentViewChange;
// goto segment
const gotoButton = document.createElement('button');
gotoButton.id = getHtmlId(prefixes.goto, segmentId);
gotoButton.title = 'Goto segment';
gotoButton.appendChild(document.createTextNode('\u{1F3AF}'));
gotoButton.onclick = this.#onGotoSegment;
// segment delete
const deleteButton = document.createElement('button');
deleteButton.id = getHtmlId(prefixes.delete, segmentId);
deleteButton.title = 'Delete segment';
deleteButton.appendChild(document.createTextNode('\u{274C}'));
deleteButton.onclick = this.#onSegmentDelete;
// segment span
const span = document.createElement('span');
span.id = getHtmlId(prefixes.span, segmentId);
span.appendChild(selectInput);
span.appendChild(selectLabel);
span.appendChild(volumesSpan);
span.appendChild(colourInput);
span.appendChild(gotoButton);
span.appendChild(viewButton);
span.appendChild(deleteButton);
return span;
}
/**
* Handle an eraser select from UI.
*
* @param {Event} event HTML event.
*/
#onEraserSelect = (event) => {
const target = event.target;
// get segmentation
const segmentationIndex = splitSegmentationHtmlId(
getRootFromHtmlId(prefixes.selectEraser, target.id));
const segmentation = _segmentations[segmentationIndex];
// select eraser
this.#appSelectEraser(segmentation);
};
/**
* Handle a segment add from UI.
*
* @param {MouseEvent} event HTML event.
*/
#onSegmentAdd = (event) => {
const target = event.target;
// get segmentation
const segmentationIndex = splitSegmentationHtmlId(
getRootFromHtmlId(prefixes.addSegment, target.id));
const segmentation = _segmentations[segmentationIndex];
const segments = segmentation.segments;
// create new segment
const newSegment = getNewSegment(segments.length + 1);
// add to list
segments.push(newSegment);
// update flag
segmentation.hasNewSegments = true;
// update UI
const actionGroup = target.parentElement;
const list = actionGroup.parentElement;
// remove action group
actionGroup.remove();
// add segment to list
list.appendChild(
this.#getSegmentHtml(newSegment, segmentationIndex)
);
// put back action group
list.appendChild(actionGroup);
};
/**
* Handle a segmentation save from UI.
*
* @param {MouseEvent} event HTML event.
*/
#onSegmentationSave = (event) => {
const target = event.target;
// get segmentation
const segmentationIndex = splitSegmentationHtmlId(
getRootFromHtmlId(prefixes.save, target.id));
const segmentationName = getSegmentationHtmlId(segmentationIndex);
const segmentation = _segmentations[segmentationIndex];
const dataId = segmentation.dataId;
// get data
const maskData = this.#app.getData(dataId);
if (typeof maskData === 'undefined') {
throw new Error('Cannot save without mask image');
}
// TODO: find better way...
const sourceId = dataId - 1;
const sourceData = this.#app.getData(sourceId.toString());
if (typeof sourceData === 'undefined') {
throw new Error('Cannot save without source image');
}
// dicom elements
const fac = new MaskFactory();
const dicomElements = fac.toDicom(
maskData.image,
maskData.image.getMeta().custom.segments,
sourceData.image,
{
MediaStorageSOPInstanceUID: '1.2.3.4.5.6',
SeriesInstanceUID: '1.2.3.4.5.6',
SeriesNumber: '1',
SOPInstanceUID: '1.2.3.4.5.6.1000',
}
);
// create writer with default rules
const writer = new DicomWriter();
let dicomBuffer;
try {
dicomBuffer = writer.getBuffer(dicomElements);
} catch (error) {
logger.error(error);
alert(error.message);
}
if (dicomBuffer !== undefined) {
// view as Blob to allow download
const blob = new Blob([dicomBuffer], {type: 'application/dicom'});
// update generate button
const element = document.createElement('a');
element.href = window.URL.createObjectURL(blob);
element.download = segmentationName + '.dcm';
// trigger download
element.click();
URL.revokeObjectURL(element.href);
}
};
/**
* Convert the labels of a segmentation into a displayable string.
*
* @param {object} segment The segment.
* @param {number} segmentationIndex The segmentation index.
* @returns {string} The display string of labels.
*/
#getLabelsString(segment, segmentationIndex) {
const segmentation = _segmentations[segmentationIndex];
const start = ' [';
let res = start;
for (const label of segmentation.labels) {
if (label.id === segment.number) {
if (res !== start) {
res += ', ';
}
res += label.volume.toPrecision(4) + i18n.t(label.unit);
}
}
res += ']';
// clear if empty
if (res === start + ']') {
res = '';
}
return res;
}
/**
* Updates the text of the labels diplay for a segmentation.
*
* @param {object} segmentation The segmentation.
*/
#updateLabelsSpan(segmentation) {
const segmentationIndex =
_segmentations.findIndex(
(seg) => {
return seg === segmentation;
}
);
if (segmentationIndex >= 0) {
for (const segment of segmentation.segments) {
const segmentId = getSegmentHtmlId(segment.number, segmentationIndex);
const spanId = getHtmlId(prefixes.volumes, segmentId);
const span = document.getElementById(spanId);
if (span) {
span.innerText = this.#getLabelsString(segment, segmentationIndex);
}
}
}
}
/**
* Get the HTML list element for a segmentation.
*
* @param {object} segmentation The segmentation.
* @param {number} segmentationIndex The segmentation index.
* @returns {HTMLLIElement} The HTML element.
*/
#getSegmentationHtml(segmentation, segmentationIndex) {
const segmentationName = getSegmentationHtmlId(segmentationIndex);
// segmentation item
const segmentationItem = document.createElement('li');
// save segmentation
const saveButton = document.createElement('button');
saveButton.appendChild(document.createTextNode('\u{1F4BE}'));
saveButton.title = 'Save segmentation';
saveButton.id = getHtmlId(prefixes.save, segmentationName);
saveButton.onclick = this.#onSegmentationSave;
segmentationItem.appendChild(saveButton);
// segmentation name
segmentationItem.appendChild(document.createTextNode(segmentationName));
// add segments
const segments = segmentation.segments;
for (let j = 0; j < segments.length; ++j) {
segmentationItem.appendChild(
this.#getSegmentHtml(segments[j], segmentationIndex)
);
}
// segment eraser
const eraserInput = document.createElement('input');
eraserInput.type = 'radio';
eraserInput.name = 'select-segment';
eraserInput.title = 'Eraser';
eraserInput.id = getHtmlId(prefixes.selectEraser, segmentationName);
eraserInput.onchange = this.#onEraserSelect;
const eraserLabel = document.createElement('label');
eraserLabel.htmlFor = eraserInput.id;
eraserLabel.title = eraserInput.title;
eraserLabel.appendChild(document.createTextNode('Eraser'));
// add segment
const addSegmentButton = document.createElement('button');
addSegmentButton.appendChild(document.createTextNode('\u2795'));
addSegmentButton.title = 'Add segment';
addSegmentButton.id = getHtmlId(prefixes.addSegment, segmentationName);
addSegmentButton.onclick = this.#onSegmentAdd;
// action span
const actionSpan = document.createElement('span');
actionSpan.id = 'span-action-' + segmentationName;
actionSpan.appendChild(eraserInput);
actionSpan.appendChild(eraserLabel);
actionSpan.appendChild(addSegmentButton);
// append span to item
segmentationItem.appendChild(actionSpan);
return segmentationItem;
}
}; // SegmentationUI