From 50cd3f10cf2d07130165a00a093b814564e5c453 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 1 Sep 2025 15:21:39 +0100 Subject: [PATCH] refactor: improve code readability and performance in viewer.js --- js/viewer.js | 318 ++++++++++++++++++++++++++++----------------------- 1 file changed, 173 insertions(+), 145 deletions(-) diff --git a/js/viewer.js b/js/viewer.js index 8ca0fac..389817e 100644 --- a/js/viewer.js +++ b/js/viewer.js @@ -69,7 +69,7 @@ export function loadMainImage(image, stack) { //element.scrollIntoView(false); //element.scrollTo(0); - $(element).dblclick(function () { + $(element).dblclick(function() { if ($(".canvas-panel").length == 0) { // already fullscreen (disable it) disableFullscreen(this); @@ -82,7 +82,7 @@ export function loadMainImage(image, stack) { $(".dicom-select-control") .get(0) - .addEventListener("change", function () { + .addEventListener("change", function() { changeControlSelection(); }); @@ -114,10 +114,10 @@ function onImageRendered(e) { $("option[value=scroll").prop("hidden", false); $("option[value=scroll").text( "scroll (" + - (stack.currentImageIdIndex + 1) + - "/" + - stack.imageIds.length + - ")" + (stack.currentImageIdIndex + 1) + + "/" + + stack.imageIds.length + + ")" ); // Temp way to enable CT window presets @@ -566,6 +566,62 @@ export function keydown_handler(event) { event.preventDefault(); } +function loadAnnotation(imageId, annotation) { + const toolStateManager = + cornerstoneTools.globalImageIdSpecificToolStateManager; + + if (annotation == undefined || annotation.length < 1) { + return; + } + + let tool_state_no_id = JSON.parse(annotation); + + let tool_state = {}; + tool_state[imageId] = tool_state_no_id; + + toolStateManager.restoreToolState(tool_state); +} +async function load(images, annotations) { + // Parallelize image preparation and annotation loading + const imageTasks = images.map(async (data_url, i) => { + const annotation = annotations[i]; + let imageId; + if (data_url.startsWith("data:image")) { + imageId = "base64://" + data_url.split(",")[1]; + } else if (data_url.startsWith("data:")) { + const dfile = await urltoFile(data_url, "dicom", "application/dicom"); + imageId = cornerstoneWADOImageLoader.wadouri.fileManager.add(dfile); + } else { + let url = data_url; + if (url.endsWith("dcm")) { + url = "wadouri:" + url; + } + if (/(?:\/|^)[^.\/]+$/.test(url)) { + url = "wadouri:" + url; + } + imageId = 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 = { + currentImageIdIndex: 0, + imageIds, + }; + loadMainImage(image, stack); + // Restore annotations for all images after display + imageResults.forEach(({imageId, annotation}) => { + loadAnnotation(imageId, annotation); + }); + }); +} + /** * View cornstone image * @param {*} t @@ -576,82 +632,7 @@ export function openMainImage(current_question, t, source) { * Load image * @param {*} images - list of images */ - function loadAnnotation(imageId, annotation) { - const toolStateManager = - cornerstoneTools.globalImageIdSpecificToolStateManager; - if (annotation == undefined || annotation.length < 1) { - return; - } - - let tool_state_no_id = JSON.parse(annotation); - - let tool_state = {}; - tool_state[imageId] = tool_state_no_id; - - toolStateManager.restoreToolState(tool_state); - } - async function load(images, annotations) { - //console.debug("Load function", images) - const imageIds = []; - for (let i = 0; i < images.length; i++) { - const data_url = images[i]; - const annotation = annotations[i]; - - // check stack type - if (data_url.startsWith("data:image")) { - const 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:")) { - // stack = stack.split(";")[1]; - - const dfile = await urltoFile(data_url, "dicom", "application/dicom"); - - const imageId = cornerstoneWADOImageLoader.wadouri.fileManager.add( - dfile - ); - - loadAnnotation(imageId, annotation); - - imageIds.push(imageId); - // cornerstone.loadImage(imageId).then(function(image) { - // tempFunction(image); - // }); - } else { - let url = data_url; - //if (data_url.startsWith("http")) { - // url = data_url; - //} else { - // url = window.location.href.replace(/\/\#\/?$/, "") + "/" + data_url; - //} - - if (url.endsWith("dcm")) { - url = "wadouri:" + url; - } - - // if there is no extension treat it as a dicom - if (/(?:\/|^)[^.\/]+$/.test(url)) { - url = "wadouri:" + url; - } - //console.debug(url) - - loadAnnotation(url, annotation); - - imageIds.push(url); - } - } - const stack = { - currentImageIdIndex: 0, - imageIds, - }; - cornerstone.loadAndCacheImage(imageIds[0]).then(function (image) { - loadMainImage(image, stack); - }); - } if (current_question) { // Check if the figure is already loaded (or if another one is) @@ -680,44 +661,91 @@ export function openMainImage(current_question, t, source) { source.className = "figure-open"; - console.log(current_question, figure_to_load, source); let image_title = source.getElementsByClassName("figcaption")[0].textContent; - $(".question").prepend( - `
-
-
-
${image_title}
-
- - - - - -
-
' - ); + // Native DOM creation for performance + const questionElem = document.querySelector('.question'); + const canvasPanel = document.createElement('div'); + canvasPanel.className = 'canvas-panel'; + + const viewerTitleBar = document.createElement('div'); + viewerTitleBar.id = 'viewer-title-bar'; + + const flexDiv = document.createElement('div'); + flexDiv.style.flex = '1 1 auto'; + + const viewerTitle = document.createElement('div'); + viewerTitle.id = 'viewer-title'; + viewerTitle.style.color = 'white'; + viewerTitle.textContent = image_title; + flexDiv.appendChild(viewerTitle); + viewerTitleBar.appendChild(flexDiv); + + // Select control + const select = document.createElement('select'); + select.title = 'SELECT CLICK ACTION'; + select.className = 'dicom-select-control title-bar-button'; + select.style.marginRight = '8px'; + const options = [ + { value: 'pan', text: 'pan [p]' }, + { value: 'zoom', text: 'zoom [z]' }, + { value: 'rotate', text: 'rotate [r]' }, + { value: 'window', text: 'window (128 ± 127.5) [w]' }, + { value: 'measure', text: 'measure [m]' }, + { value: 'ellipse', text: 'ellipse [l]' }, + { value: 'rectangle', text: 'rectangle [c]' }, + { value: 'reset', text: 'reset [e]' }, + { value: 'notes', text: '[modality = SC]', disabled: true } + ]; + 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 = ``; + const svgFullscreen = ``; + const svgFullscreenExit = ``; + const svgClose = ``; + + 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]]; // images = current_question.images @@ -737,16 +765,16 @@ export function openMainImage(current_question, t, source) { const dicom_element = document.getElementById("dicom-image"); // Bind button actions - $("#close-viewer-button").click(function () { + $("#close-viewer-button").click(function() { closeViewer(dicom_element); }); - $("#reset-viewer-button").click(function () { + $("#reset-viewer-button").click(function() { resetViewer(dicom_element); }); - $("#fullscreen-viewer-button").click(function () { + $("#fullscreen-viewer-button").click(function() { enableFullscreen(dicom_element); }); - $("#disable-fullscreen-viewer-button").click(function () { + $("#disable-fullscreen-viewer-button").click(function() { disableFullscreen(dicom_element); }); @@ -755,39 +783,39 @@ export function openMainImage(current_question, t, source) { } function closeViewer(dicom_element) { - // disable fullscreen if required - if ($(".canvas-panel").length == 0) { - disableFullscreen(dicom_element); - } - if (dicom_element != undefined) { - // See https://github.com/cornerstonejs/cornerstoneTools/issues/1337 - cornerstone.removeElementData(dicom_element); - cornerstone.disable(dicom_element); - $(".canvas-panel").remove(); - $(".figure-open").removeClass("figure-open").addClass("figure"); - $(dicom_element).remove(); - } + // disable fullscreen if required + if ($(".canvas-panel").length == 0) { + disableFullscreen(dicom_element); + } + if (dicom_element != undefined) { + // See https://github.com/cornerstonejs/cornerstoneTools/issues/1337 + cornerstone.removeElementData(dicom_element); + cornerstone.disable(dicom_element); + $(".canvas-panel").remove(); + $(".figure-open").removeClass("figure-open").addClass("figure"); + $(dicom_element).remove(); + } } export function urltoFile(url, filename, mimeType) { return fetch(url) - .then(function (res) { + .then(function(res) { return res.arrayBuffer(); }) - .then(function (buf) { - return new File([buf], filename, { type: mimeType }); + .then(function(buf) { + return new File([buf], filename, {type: mimeType}); }); } function enableFullscreen(dicom_element) { - $(".content-panel").prepend($(".canvas-panel")); - $(".canvas-panel").toggleClass("canvas-panel canvas-panel-fullscreen"); - $("#dicom-image").attr("height", "100%"); - $("#dicom-image").height("100%"); - $("#disable-fullscreen-viewer-button").show(); - $("#fullscreen-viewer-button").hide(); - // $(".cornerstone-canvas").attr("height", "100%"); - // $(".cornerstone-canvas").height("100%"); - cornerstone.resize(dicom_element, true); + $(".content-panel").prepend($(".canvas-panel")); + $(".canvas-panel").toggleClass("canvas-panel canvas-panel-fullscreen"); + $("#dicom-image").attr("height", "100%"); + $("#dicom-image").height("100%"); + $("#disable-fullscreen-viewer-button").show(); + $("#fullscreen-viewer-button").hide(); + // $(".cornerstone-canvas").attr("height", "100%"); + // $(".cornerstone-canvas").height("100%"); + cornerstone.resize(dicom_element, true); } \ No newline at end of file