/* global cornerstone, cornerstoneTools, cornerstoneBase64ImageLoader, cornerstoneWebImageLoader, cornerstoneWADOImageLoader */ /** * Load the main image * @param {*} image - * @param {*} stack - */ export function loadMainImage(image, stack) { const PanTool = cornerstoneTools.PanTool; const ZoomTool = cornerstoneTools.ZoomTool; const WwwcTool = cornerstoneTools.WwwcTool; const RotateTool = cornerstoneTools.RotateTool; const StackScrollTool = cornerstoneTools.StackScrollTool; const ArrowAnnotateTool = cornerstoneTools.ArrowAnnotateTool; const LengthTool = cornerstoneTools.LengthTool; const RectangleRoiTool = cornerstoneTools.RectangleRoiTool; const EllipseRoiTool = cornerstoneTools.EllipseRoiTool; const AngleTool = cornerstoneTools.AngleTool; const CobbAngleTool = cornerstoneTools.CobbAngleTool; const EraserTool = cornerstoneTools.EraserTool; const element = document.getElementById("dicom-image"); cornerstone.enable(element); cornerstone.displayImage(element, image); cornerstoneTools.addStackStateManager(element, ["stack"]); cornerstoneTools.addToolState(element, "stack", stack); cornerstoneTools.addToolForElement(element, PanTool); cornerstoneTools.addToolForElement(element, ZoomTool, { configuration: { invert: true, }, }); cornerstoneTools.addToolForElement(element, WwwcTool); cornerstoneTools.addToolForElement(element, RotateTool); cornerstoneTools.addToolForElement(element, AngleTool); cornerstoneTools.addToolForElement(element, CobbAngleTool); cornerstoneTools.addToolForElement(element, StackScrollTool); cornerstoneTools.addToolForElement(element, EraserTool); cornerstoneTools.addToolForElement(element, LengthTool); cornerstoneTools.addToolForElement(element, RectangleRoiTool); //cornerstoneTools.addToolForElement(element, EllipseRoiTool); cornerstoneTools.addToolForElement(element, ArrowAnnotateTool, { configuration: { getTextCallback: () => {}, changeTextCallback: () => {}, allowEmptyLabel: true, renderDashed: false, drawHandles: false, drawHandlesOnHover: true, }, }); cornerstoneTools.setToolActiveForElement(element, "Pan", { mouseButtonMask: 1, }); cornerstoneTools.setToolEnabledForElement(element, "ArrowAnnotate"); //cornerstoneTools.keyboardInput.enable(element); element.addEventListener("cornerstoneimagerendered", onImageRendered); setDicomCanvasNonFullscreen(element); cornerstone.reset(element); let viewerTitleBar = document.getElementById("viewer-title-bar"); viewerTitleBar.scrollIntoView(); //element.scrollIntoView(false); //element.scrollTo(0); $(element).dblclick(function() { if ($(".canvas-panel").length == 0) { // already fullscreen (disable it) disableFullscreen(this); } else { enableFullscreen(this); } }); element.removeEventListener("wheel", element.wheelEventHandler); $(".dicom-select-control") .get(0) .addEventListener("change", function() { changeControlSelection(); }); // Register Key Event Listener document.addEventListener("keydown", keydown_handler); } /** * Called when a dicom image is rendered * @param {*} e - Event */ function onImageRendered(e) { const eventData = e.detail; // Update ww/wl const sel = $(".dicom-select-control").get(0); sel.options[find_option(sel, "window")].firstChild.textContent = "window (" + Math.round(eventData.viewport.voi.windowCenter) + " \u00b1 " + Math.round(eventData.viewport.voi.windowWidth) + ")"; // update stack data const stack = eventData.enabledElement.toolStateManager.toolState.stack.data[0]; if (stack.imageIds.length > 1) { $("option[value=scroll").prop("disabled", false); $("option[value=scroll").prop("hidden", false); $("option[value=scroll").text( "scroll (" + (stack.currentImageIdIndex + 1) + "/" + stack.imageIds.length + ")" ); // Temp way to enable CT window presets // Enable for all stacked images... $("option[value=abdomen").prop("hidden", false); $("option[value=pulmonary").prop("hidden", false); $("option[value=brain").prop("hidden", false); $("option[value=bone").prop("hidden", false); $("option[value=abdomen").prop("disabled", false); $("option[value=pulmonary").prop("disabled", false); $("option[value=brain").prop("disabled", false); $("option[value=bone").prop("disabled", false); } } /** * Finds a select options but its text value * @param {*} select - select element * @param {*} text - text value * @return {number} - number of found option or 'undefined' */ export function find_option(select, text) { for (let i = 0; i < select.length; ++i) { if (select.options[i].value == text) { return i; } } return undefined; } /** * Disables fullscreen mode * @param {*} dicom_element - */ export function disableFullscreen(dicom_element) { // TODO: rescale the image to stop it getting zoomed in $(".canvas-panel-fullscreen").toggleClass( "canvas-panel canvas-panel-fullscreen" ); $("#disable-fullscreen-viewer-button").hide(); $("#fullscreen-viewer-button").show(); $(".question").prepend($(".canvas-panel")); $(".canvas-panel").get(0).scrollIntoView(); setDicomCanvasNonFullscreen(dicom_element); } /** * Enable fullscreen * @param {*} element - */ export function setDicomCanvasNonFullscreen(element) { const h = window.innerHeight - $(".nav-bar").height() - 4; $("#dicom-image").attr("height", h); $("#dicom-image").height(h); // $("#dicom-image.cornerstone-canvas").attr("height", h); // $("#dicom-image.cornerstone-canvas").height(h); $("#dicom-image .cornerstone-canvas").attr("width", "100%"); cornerstone.resize(element, true); } export function manualPanDicom(x, y) { let dicom_element = document.getElementById("dicom-image"); const viewport = cornerstone.getViewport(dicom_element); viewport.translation.x += x; viewport.translation.y += y; cornerstone.setViewport(dicom_element, viewport); } export function manualZoomDicom(delta) { let dicom_element = document.getElementById("dicom-image"); const viewport = cornerstone.getViewport(dicom_element); viewport.scale += delta / 100; cornerstone.setViewport(dicom_element, viewport); } export function manualRotateDicom(delta) { let dicom_element = document.getElementById("dicom-image"); const viewport = cornerstone.getViewport(dicom_element); viewport.rotation += delta; cornerstone.setViewport(dicom_element, viewport); } export function manualScrollDicom(n) { // There must be a better way to do this... let dicom_element = document.getElementById("dicom-image"); let c = cornerstone.getEnabledElement(dicom_element); let max = c.toolStateManager.toolState.stack.data[0].imageIds.length; if (max < 2) { return; } let current_index = c.toolStateManager.toolState.stack.data[0].currentImageIdIndex; // Loop through stack if out of bounds // Yes it is annoying but it's the way it is done... let new_index = current_index + n; if (new_index >= max) { new_index = new_index - max; } else if (new_index < 0) { new_index = new_index + max; } c.toolStateManager.toolState.stack.data[0].currentImageIdIndex = new_index; let id = c.toolStateManager.toolState.stack.data[0].imageIds[new_index]; cornerstone.loadImage(id).then((b) => { cornerstone.displayImage(dicom_element, b); }); // c = cornerstone.getEnabledElement(dicom_element) } export function manualWindowDicom(wl, ww) { let dicom_element = document.getElementById("dicom-image"); const viewport = cornerstone.getViewport(dicom_element); // For reference // brightness ~= wl // contrast ~= ww viewport.voi.windowCenter += wl; viewport.voi.windowWidth += ww; cornerstone.setViewport(dicom_element, viewport); } export function debugCornerstone() { let dicom_element = document.getElementById("dicom-image"); let viewport = cornerstone.getViewport(dicom_element); let c = cornerstone.getEnabledElement(dicom_element); } /** * Registers the selected dicom tool * * This also allows selecting of window levels */ export function changeControlSelection() { // We also duplicate the sel that are available const sel = $(".dicom-select-control").get(0); const old = sel.oldSelectedIndex; sel.oldSelectedIndex = sel.selectedIndex; const dicom_element = document.getElementById("dicom-image"); switch (sel.options[sel.selectedIndex].value) { case "pan": { cornerstoneTools.setToolActiveForElement(dicom_element, "Pan", { mouseButtonMask: 1, }); break; } case "zoom": { cornerstoneTools.setToolActiveForElement(dicom_element, "Zoom", { mouseButtonMask: 1, }); break; } case "rotate": { cornerstoneTools.setToolActiveForElement(dicom_element, "Rotate", { mouseButtonMask: 1, }); break; } case "measure": { cornerstoneTools.setToolActiveForElement(dicom_element, "Length", { mouseButtonMask: 1, }); break; } case "rectangle": { cornerstoneTools.setToolActiveForElement(dicom_element, "RectangleRoi", { mouseButtonMask: 1, }); break; } case "ellipse": { cornerstoneTools.setToolActiveForElement(dicom_element, "EllipseRoi", { mouseButtonMask: 1, }); break; } case "angle": { cornerstoneTools.setToolActiveForElement(dicom_element, "Angle", { mouseButtonMask: 1, }); break; } case "cobbangle": { cornerstoneTools.setToolActiveForElement(dicom_element, "CobbAngle", { mouseButtonMask: 1, }); break; } case "eraser": { cornerstoneTools.setToolActiveForElement(dicom_element, "Eraser", { mouseButtonMask: 1, }); break; } case "scroll": { cornerstoneTools.setToolActiveForElement(dicom_element, "StackScroll", { mouseButtonMask: 1, }); // _this.left.onstart = _this.scroll_start; // _this.selected_control = t.selectedIndex; break; } case "window": { cornerstoneTools.setToolActiveForElement(dicom_element, "Wwwc", { mouseButtonMask: 1, }); break; } case "abdomen": { const viewport = cornerstone.getViewport(dicom_element); viewport.voi.windowCenter = 150; viewport.voi.windowWidth = 500; cornerstone.setViewport(dicom_element, viewport); sel.selectedIndex = find_option(sel, "window"); changeControlSelection(); break; } case "pulmonary": { const viewport = cornerstone.getViewport(dicom_element); viewport.voi.windowCenter = -500; viewport.voi.windowWidth = 1500; cornerstone.setViewport(dicom_element, viewport); sel.selectedIndex = find_option(sel, "window"); changeControlSelection(); break; } case "brain": { const viewport = cornerstone.getViewport(dicom_element); viewport.voi.windowCenter = 50; viewport.voi.windowWidth = 80; cornerstone.setViewport(dicom_element, viewport); sel.selectedIndex = find_option(sel, "window"); changeControlSelection(); break; } case "bone": { const viewport = cornerstone.getViewport(dicom_element); viewport.voi.windowCenter = 570; viewport.voi.windowWidth = 3000; cornerstone.setViewport(dicom_element, viewport); sel.selectedIndex = find_option(sel, "window"); changeControlSelection(); break; } case "reset": { resetViewer(dicom_element); sel.selectedIndex = old; sel.oldSelectedIndex = old; break; } case "close": { closeViewer(dicom_element); break; } } sel.blur(); if (event) { event.preventDefault(); } } function resetViewer(dicom_element) { cornerstoneTools.clearToolState(dicom_element, "RectangleRoi"); cornerstoneTools.clearToolState(dicom_element, "Length"); cornerstone.reset(dicom_element); } /** * Handle key button events * These are the same as in the RCR pratique implementation * @param {*} event - KeyEvent */ export function keydown_handler(event) { //console.debug(event); const target_element = event.target.tagName; // Cancel if no active element let dicom_element = document.getElementById("dicom-image"); if (dicom_element == undefined) { return; } // Catch all keypresses unless user is typing if (target_element == "INPUT" || target_element == "TEXTAREA") { return; } const sel = $(".dicom-select-control").get(0); switch (event.code) { case "KeyP": sel.selectedIndex = find_option(sel, "pan"); changeControlSelection(); break; case "KeyC": sel.selectedIndex = find_option(sel, "rectangle"); changeControlSelection(); break; case "KeyZ": sel.selectedIndex = find_option(sel, "zoom"); changeControlSelection(); break; case "KeyR": sel.selectedIndex = find_option(sel, "rotate"); changeControlSelection(); break; case "KeyS": sel.selectedIndex = find_option(sel, "scroll"); changeControlSelection(); break; case "KeyW": sel.selectedIndex = find_option(sel, "window"); changeControlSelection(); break; case "KeyA": sel.selectedIndex = find_option(sel, "abdomen"); changeControlSelection(); break; case "KeyU": sel.selectedIndex = find_option(sel, "pulmonary"); changeControlSelection(); break; case "KeyB": sel.selectedIndex = find_option(sel, "brain"); changeControlSelection(); break; case "KeyO": sel.selectedIndex = find_option(sel, "bone"); changeControlSelection(); break; case "KeyE": sel.selectedIndex = find_option(sel, "reset"); // $("#dicom-image").get(0).scrollIntoView(false); changeControlSelection(); break; // Escape and C do the same //case "KeyC": case "Escape": sel.selectedIndex = find_option(sel, "close"); changeControlSelection(); break; case "Enter": t.focus(); break; // TODO: implement arrow stuff // case "ArrowUp": { // Arrow function depends on the currently selected option switch (sel.options[sel.selectedIndex].value) { case "pan": manualPanDicom(0, -1); break; case "zoom": // _this.current_dicom.current_view.pre_scale_at(1.01, 1.01, _this.current_dicom.cols / 2, _this.current_dicom.rows / 2); manualZoomDicom(1); break; case "rotate": manualRotateDicom(-1); break; case "scroll": manualScrollDicom(-1); break; case "window": manualWindowDicom(1, 0); break; } break; } case "ArrowDown": { switch (sel.options[sel.selectedIndex].value) { case "pan": manualPanDicom(0, 1); break; case "zoom": manualZoomDicom(-1); break; case "rotate": manualRotateDicom(1); break; case "scroll": manualScrollDicom(1); break; case "window": manualWindowDicom(-1, 0); break; } break; } case "ArrowLeft": { switch (sel.options[sel.selectedIndex].value) { case "pan": manualPanDicom(-1, 0); break; case "zoom": manualZoomDicom(-1); break; case "rotate": manualRotateDicom(-1); break; case "scroll": manualScrollDicom(1); break; case "window": manualWindowDicom(0, -1); break; } break; } case "ArrowRight": { switch (sel.options[sel.selectedIndex].value) { case "pan": manualPanDicom(1, 0); break; case "zoom": manualZoomDicom(1); break; case "rotate": manualRotateDicom(1); break; case "scroll": manualScrollDicom(-1); break; case "window": manualWindowDicom(0, 1); break; } break; } } 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 * @param {*} source */ export function openMainImage(current_question, t, source) { /** * Load image * @param {*} images - list of images */ if (current_question) { // Check if the figure is already loaded (or if another one is) const open_figure = $("#dicom-image").data("figure"); const figure_to_load = source.id; if (open_figure != undefined) { // No full size figure / dicom loaded yet if (figure_to_load == open_figure) { // Scroll to the image if it is already open document.getElementById("viewer-title-bar").scrollIntoView(); return; } else { let el; el = document.getElementById("dicom-image"); if (el != undefined) { cornerstone.disable(el); // Explicitly remove the dicom-image element el.remove(); $(".canvas-panel").remove(); } } } $(".figure-open").removeClass("figure-open").addClass("figure"); source.className = "figure-open"; let image_title = source.getElementsByClassName("figcaption")[0].textContent; // 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: 'scroll', text: 'scroll [s]' }, { value: 'window', text: 'window (128 ± 127.5) [w]' }, { value: 'abdomen', text: 'window = abdomen [a]', hidden: true, disabled: true }, { value: 'pulmonary', text: 'window = pulmonary [p]', hidden: true, disabled: true }, { value: 'brain', text: 'window = brain [b]', hidden: true, disabled: true }, { value: 'bone', text: 'window = bone [o]', hidden: true, disabled: true }, { 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; if (opt.hidden) o.style.display = 'none'; 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 // Make sure we have an array if (!Array.isArray(images)) { images = [images]; } let annotations = []; if (current_question.annotations) { annotations = current_question.annotations[figure_to_load.split("-")[1]]; if (!Array.isArray(annotations)) { annotations = [annotations]; } } const dicom_element = document.getElementById("dicom-image"); // Bind button actions $("#close-viewer-button").click(function() { closeViewer(dicom_element); }); $("#reset-viewer-button").click(function() { resetViewer(dicom_element); }); $("#fullscreen-viewer-button").click(function() { enableFullscreen(dicom_element); }); $("#disable-fullscreen-viewer-button").click(function() { disableFullscreen(dicom_element); }); load(images, annotations); } } 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(); } } export function urltoFile(url, filename, mimeType) { return fetch(url) .then(function(res) { return res.arrayBuffer(); }) .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); }