refactor: improve code readability and performance in viewer.js

This commit is contained in:
Ross
2025-09-01 15:21:39 +01:00
parent 02162d6aea
commit 50cd3f10cf
+114 -86
View File
@@ -566,16 +566,6 @@ export function keydown_handler(event) {
event.preventDefault(); event.preventDefault();
} }
/**
* View cornstone image
* @param {*} t
* @param {*} source
*/
export function openMainImage(current_question, t, source) {
/**
* Load image
* @param {*} images - list of images
*/
function loadAnnotation(imageId, annotation) { function loadAnnotation(imageId, annotation) {
const toolStateManager = const toolStateManager =
cornerstoneTools.globalImageIdSpecificToolStateManager; cornerstoneTools.globalImageIdSpecificToolStateManager;
@@ -592,67 +582,58 @@ export function openMainImage(current_question, t, source) {
toolStateManager.restoreToolState(tool_state); toolStateManager.restoreToolState(tool_state);
} }
async function load(images, annotations) { async function load(images, annotations) {
//console.debug("Load function", images) // Parallelize image preparation and annotation loading
const imageIds = []; const imageTasks = images.map(async (data_url, i) => {
for (let i = 0; i < images.length; i++) {
const data_url = images[i];
const annotation = annotations[i]; const annotation = annotations[i];
let imageId;
// check stack type
if (data_url.startsWith("data:image")) { if (data_url.startsWith("data:image")) {
const imageId = "base64://" + data_url.split(",")[1]; imageId = "base64://" + data_url.split(",")[1];
loadAnnotation(imageId, annotation);
imageIds.push(imageId);
//} else if (data_url.startsWith("data:application/dicom")) {
} else if (data_url.startsWith("data:")) { } else if (data_url.startsWith("data:")) {
// stack = stack.split(";")[1];
const dfile = await urltoFile(data_url, "dicom", "application/dicom"); const dfile = await urltoFile(data_url, "dicom", "application/dicom");
imageId = cornerstoneWADOImageLoader.wadouri.fileManager.add(dfile);
const imageId = cornerstoneWADOImageLoader.wadouri.fileManager.add(
dfile
);
loadAnnotation(imageId, annotation);
imageIds.push(imageId);
// cornerstone.loadImage(imageId).then(function(image) {
// tempFunction(image);
// });
} else { } else {
let url = data_url; let url = data_url;
//if (data_url.startsWith("http")) {
// url = data_url;
//} else {
// url = window.location.href.replace(/\/\#\/?$/, "") + "/" + data_url;
//}
if (url.endsWith("dcm")) { if (url.endsWith("dcm")) {
url = "wadouri:" + url; url = "wadouri:" + url;
} }
// if there is no extension treat it as a dicom
if (/(?:\/|^)[^.\/]+$/.test(url)) { if (/(?:\/|^)[^.\/]+$/.test(url)) {
url = "wadouri:" + url; url = "wadouri:" + url;
} }
//console.debug(url) imageId = url;
loadAnnotation(url, annotation);
imageIds.push(url);
}
} }
return {imageId, annotation};
});
// Wait for all image tasks to complete
const imageResults = await Promise.all(imageTasks);
const imageIds = imageResults.map(r => r.imageId);
// Display the first image as soon as possible
cornerstone.loadAndCacheImage(imageIds[0]).then(function(image) {
const stack = { const stack = {
currentImageIdIndex: 0, currentImageIdIndex: 0,
imageIds, imageIds,
}; };
cornerstone.loadAndCacheImage(imageIds[0]).then(function (image) {
loadMainImage(image, stack); loadMainImage(image, stack);
// Restore annotations for all images after display
imageResults.forEach(({imageId, annotation}) => {
loadAnnotation(imageId, annotation);
});
}); });
} }
/**
* View cornstone image
* @param {*} t
* @param {*} source
*/
export function openMainImage(current_question, t, source) {
/**
* Load image
* @param {*} images - list of images
*/
if (current_question) { if (current_question) {
// Check if the figure is already loaded (or if another one is) // Check if the figure is already loaded (or if another one is)
const open_figure = $("#dicom-image").data("figure"); const open_figure = $("#dicom-image").data("figure");
@@ -680,44 +661,91 @@ export function openMainImage(current_question, t, source) {
source.className = "figure-open"; source.className = "figure-open";
console.log(current_question, figure_to_load, source);
let image_title = source.getElementsByClassName("figcaption")[0].textContent; let image_title = source.getElementsByClassName("figcaption")[0].textContent;
$(".question").prepend( // Native DOM creation for performance
`<div class="canvas-panel"> const questionElem = document.querySelector('.question');
<div id="viewer-title-bar"> const canvasPanel = document.createElement('div');
<div style="flex: 1 1 auto;"> canvasPanel.className = 'canvas-panel';
<div id="viewer-title" style="color: white;">${image_title}</div>
</div> const viewerTitleBar = document.createElement('div');
<select title="SELECT CLICK ACTION" class="dicom-select-control title-bar-button" style="margin-right: 8px;"> viewerTitleBar.id = 'viewer-title-bar';
<option value="pan">pan [p]</option>
<option value="zoom">zoom [z]</option> const flexDiv = document.createElement('div');
<option value="rotate">rotate [r]</option> flexDiv.style.flex = '1 1 auto';
<option value="window">window (128 ± 127.5) [w]</option>
<option value="measure">measure [m]</option> const viewerTitle = document.createElement('div');
<option value="ellipse">ellipse [l]</option> viewerTitle.id = 'viewer-title';
<option value="rectangle">rectangle [c]</option> viewerTitle.style.color = 'white';
<option value="reset">reset [e]</option> viewerTitle.textContent = image_title;
<option value="notes" disabled="true">[modality = SC]</option> flexDiv.appendChild(viewerTitle);
</select> viewerTitleBar.appendChild(flexDiv);
<button id="reset-viewer-button" class="title-bar-button" title="RESET VIEW" class="reset" style="margin-right: 8px;"><span class="viewer-icon"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2z"/> // Select control
<path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466"/> const select = document.createElement('select');
</svg></span></button> select.title = 'SELECT CLICK ACTION';
<button id="fullscreen-viewer-button" class="title-bar-button" title="TOGGLE FULLSCREEN" class="expand" style="margin-right: 8px;"><span class="viewer-icon"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-fullscreen" viewBox="0 0 16 16"> select.className = 'dicom-select-control title-bar-button';
<path d="M1.5 1a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4A1.5 1.5 0 0 1 1.5 0h4a.5.5 0 0 1 0 1zM10 .5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 16 1.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5M.5 10a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 0 14.5v-4a.5.5 0 0 1 .5-.5m15 0a.5.5 0 0 1 .5.5v4a1.5 1.5 0 0 1-1.5 1.5h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5"/> select.style.marginRight = '8px';
</svg></span></button> const options = [
<button style="display:none; margin-right: 8px;" id="disable-fullscreen-viewer-button" class="title-bar-button" title="TOGGLE FULLSCREEN" class="expand"><span class="viewer-icon"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-fullscreen-exit" viewBox="0 0 16 16"> { value: 'pan', text: 'pan [p]' },
<path d="M5.5 0a.5.5 0 0 1 .5.5v4A1.5 1.5 0 0 1 4.5 6h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5m5 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 10 4.5v-4a.5.5 0 0 1 .5-.5M0 10.5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 6 11.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5m10 1a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0z"/> { value: 'zoom', text: 'zoom [z]' },
</svg></span></button> { value: 'rotate', text: 'rotate [r]' },
<button id="close-viewer-button" class="title-bar-button" title="CLOSE VIEWER"><span class="viewer-icon"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x-lg" viewBox="0 0 16 16"> { value: 'window', text: 'window (128 ± 127.5) [w]' },
<path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z"/> { value: 'measure', text: 'measure [m]' },
</svg></span></button> { value: 'ellipse', text: 'ellipse [l]' },
</div> { value: 'rectangle', text: 'rectangle [c]' },
<div id="dicom-image" data-figure="` + { value: 'reset', text: 'reset [e]' },
figure_to_load + { value: 'notes', text: '[modality = SC]', disabled: true }
'"></div></div>' ];
); options.forEach(opt => {
const o = document.createElement('option');
o.value = opt.value;
o.textContent = opt.text;
if (opt.disabled) o.disabled = true;
select.appendChild(o);
});
viewerTitleBar.appendChild(select);
// Buttons
function createButton(id, title, svg, style = '') {
const btn = document.createElement('button');
btn.id = id;
btn.className = 'title-bar-button';
btn.title = title;
btn.style.marginRight = '8px';
if (style) btn.style.display = style;
const span = document.createElement('span');
span.className = 'viewer-icon';
span.innerHTML = svg;
btn.appendChild(span);
return btn;
}
// SVGs
const svgReset = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2z"/><path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466"/></svg>`;
const svgFullscreen = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-fullscreen" viewBox="0 0 16 16"><path d="M1.5 1a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4A1.5 1.5 0 0 1 1.5 0h4a.5.5 0 0 1 0 1zM10 .5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 16 1.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5M.5 10a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 0 14.5v-4a.5.5 0 0 1 .5-.5m15 0a.5.5 0 0 1 .5.5v4a1.5 1.5 0 0 1-1.5 1.5h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0-.5-.5v-4a.5.5 0 0 1 .5-.5"/></svg>`;
const svgFullscreenExit = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-fullscreen-exit" viewBox="0 0 16 16"><path d="M5.5 0a.5.5 0 0 1 .5.5v4A1.5 1.5 0 0 1 4.5 6h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5m5 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 10 4.5v-4a.5.5 0 0 1 .5-.5M0 10.5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 6 11.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5m10 1a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0z"/></svg>`;
const svgClose = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x-lg" viewBox="0 0 16 16"><path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z"/></svg>`;
const btnReset = createButton('reset-viewer-button', 'RESET VIEW', svgReset);
const btnFullscreen = createButton('fullscreen-viewer-button', 'TOGGLE FULLSCREEN', svgFullscreen);
const btnFullscreenExit = createButton('disable-fullscreen-viewer-button', 'TOGGLE FULLSCREEN', svgFullscreenExit, 'none');
const btnClose = createButton('close-viewer-button', 'CLOSE VIEWER', svgClose);
viewerTitleBar.appendChild(btnReset);
viewerTitleBar.appendChild(btnFullscreen);
viewerTitleBar.appendChild(btnFullscreenExit);
viewerTitleBar.appendChild(btnClose);
canvasPanel.appendChild(viewerTitleBar);
// DICOM image container
const dicomImageDiv = document.createElement('div');
dicomImageDiv.id = 'dicom-image';
dicomImageDiv.setAttribute('data-figure', figure_to_load);
canvasPanel.appendChild(dicomImageDiv);
// Prepend to question
questionElem.insertBefore(canvasPanel, questionElem.firstChild);
let images = current_question.images[figure_to_load.split("-")[1]]; let images = current_question.images[figure_to_load.split("-")[1]];
// images = current_question.images // images = current_question.images