From 8de4efc3e249d5e197a115574299b1b74b0bfd7c Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 15 Sep 2025 12:25:57 +0100 Subject: [PATCH] refactor image stack handling to support richer data formats and improve clarity --- dicom-viewer/src/App.tsx | 87 +++++++++++++++++++++++++++------------ dicom-viewer/src/main.tsx | 77 +++++++++++++++++++++++++++++++++- 2 files changed, 136 insertions(+), 28 deletions(-) diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx index bf2653f..fbf2d4e 100644 --- a/dicom-viewer/src/App.tsx +++ b/dicom-viewer/src/App.tsx @@ -63,7 +63,13 @@ declare global { } } -type StackEntry = { imageIds: string[], studyInstanceUID?: string }; +type StackEntry = { + imageIds: string[], + studyInstanceUID?: string, + name?: string, + caseId?: string, +}; + // Add this type for clarity declare global { @@ -145,7 +151,7 @@ const ALL_MOUSE_BINDINGS = [ type AppProps = { container_id?: string; - imageStacks: () => Promise; + imageStacks: () => Promise>; autoCacheStack?: boolean; // <-- new prop annotationJson?: string; viewerState?: string; @@ -251,34 +257,63 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer }, [annotationJson, viewerState, container_id]); // Load available stacks on mount - useEffect(() => { - imageStacks().then(async stacks => { - // Try to extract StudyInstanceUID for each stack - const stackEntries: StackEntry[] = await Promise.all( - stacks.map(async imageIds => { - let studyInstanceUID: string | undefined = undefined; - try { - // Try to fetch the first image as a blob and parse it - const firstId = imageIds[0]; - if (firstId && firstId.startsWith("wadouri:")) { - const url = firstId.slice(8); - const response = await fetch(url); - const arrayBuffer = await response.arrayBuffer(); - const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer)); - studyInstanceUID = dataSet.string('x0020000d'); - } - } catch (e) { - // Ignore errors, fallback to undefined +useEffect(() => { + let mounted = true; + + imageStacks() + .then(async (descriptors) => { + if (!mounted) return; + const stacksRaw = Array.isArray(descriptors) ? descriptors : []; + const normalized: Array = await Promise.all( + stacksRaw.map(async (d) => { + if (Array.isArray(d)) { + return { imageIds: d as string[] }; } - return { imageIds, studyInstanceUID }; + const imageIds = Array.isArray((d as any).imageIds) ? (d as any).imageIds : (Array.isArray((d as any).i) ? (d as any).i : []); + // try to preserve old studyInstanceUID extraction if needed (best-effort) + let studyInstanceUID: string | undefined = (d as any).studyInstanceUID; + // optional: attempt to fetch first file and extract StudyInstanceUID if dicomParser available + if (!studyInstanceUID && imageIds.length > 0) { + try { + const first = imageIds[0]; + if (typeof fetch === 'function' && typeof (window as any).dicomParser !== 'undefined' && first.startsWith('wadouri:')) { + const url = first.slice(7); + const resp = await fetch(url); + const buffer = await resp.arrayBuffer(); + const ds = (window as any).dicomParser.parseDicom(new Uint8Array(buffer)); + studyInstanceUID = ds.string('x0020000d'); + } + } catch (e) { + // ignore UID extraction failures + } + } + return { + imageIds, + studyInstanceUID, + name: (d as any).name, + caseId: (d as any).caseId, + }; }) ); - setAvailableStacks(stackEntries); - //setAvailableStacks(stacks.map(imageIds => ({ imageIds, studyInstanceUID: undefined }))); - await setLoadedImageIdsAndCacheMeta(stacks[0]); - setViewportImageIds(Array(MAX_VIEWPORTS).fill(stacks[0] || [])); + + setAvailableStacks(normalized); + + // Seed viewports with first stack if present (preserve old behavior) + if (normalized[0]) { + await setLoadedImageIdsAndCacheMeta(normalized[0].imageIds); + setViewportImageIds(Array(MAX_VIEWPORTS).fill(normalized[0].imageIds || [])); + } else { + setViewportImageIds(Array(MAX_VIEWPORTS).fill([])); + } + }) + .catch(err => { + console.error("Failed to load imageStacks:", err); + setAvailableStacks([]); + setViewportImageIds(Array(MAX_VIEWPORTS).fill([])); }); - }, []); + + return () => { mounted = false; }; +}, []); const handleLoadStack = async (viewportIdx: number, stackIdx: number) => { const stackObj = availableStacks[stackIdx]; diff --git a/dicom-viewer/src/main.tsx b/dicom-viewer/src/main.tsx index c21e07d..cde20d2 100644 --- a/dicom-viewer/src/main.tsx +++ b/dicom-viewer/src/main.tsx @@ -10,6 +10,12 @@ import { createImageIds, availableImageIds } from "./lib/createImageIds.ts" const toWadouri = (arr: string[]) => arr.map(url => url.startsWith("wadouri:") ? url : `wadouri:${url}`); +// Normalize a simple array -> descriptor object +const toDescriptor = (arr: string[] | undefined) => { + if (!arr) return { imageIds: [] as string[] }; + return { imageIds: toWadouri(arr) }; +}; + export function mountDicomViewers() { // Test containers const test_containers = document.querySelectorAll(".dicom-viewer-test-root"); @@ -28,10 +34,76 @@ export function mountDicomViewers() { const containers = document.querySelectorAll(".dicom-viewer-root"); containers.forEach((container) => { const id = container.id || ''; - let imageStacks; + let imageStacks: () => Promise; + // New: support data-named-stacks (richer format) and fallback to data-images + const dataNamed = container.getAttribute('data-named-stacks'); const dataImages = container.getAttribute('data-images'); - if (dataImages) { + + const parseToDescriptors = (parsed: any): Array => { + // Return array of descriptors: either simple string[] => { imageIds: string[] } + // or objects: { imageIds: string[], name?, caseId? } + if (!parsed) return []; + + // If top-level is an array of strings -> single unnamed stack + if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") { + return [toDescriptor(parsed)]; + } + + if (Array.isArray(parsed)) { + const out: any[] = []; + parsed.forEach(item => { + // Case object with nested stacks: { caseId, stacks: [ { name, imageIds } ] } + if (item && typeof item === 'object' && Array.isArray(item.stacks)) { + const caseId = item.caseId || item.caseUID || item.case || undefined; + item.stacks.forEach((s: any) => { + let imageIds: string[] = []; + if (Array.isArray(s)) imageIds = s; + else if (Array.isArray(s.imageIds)) imageIds = s.imageIds; + else if (Array.isArray(s.i)) imageIds = s.i; + out.push({ + imageIds: toWadouri(imageIds), + name: s.name || s.label || undefined, + caseId, + }); + }); + return; + } + + // Stack object: { imageIds, i, name, caseId } + if (item && typeof item === 'object' && (Array.isArray(item.imageIds) || Array.isArray(item.i))) { + const imageIds = Array.isArray(item.imageIds) ? item.imageIds : item.i; + out.push({ + imageIds: toWadouri(imageIds), + name: item.name || item.label || undefined, + caseId: item.caseId || item.caseUID || undefined, + }); + return; + } + + // Plain array (stack) + if (Array.isArray(item) && item.length > 0 && typeof item[0] === "string") { + out.push(toDescriptor(item)); + return; + } + }); + return out; + } + + return []; + }; + + if (dataNamed) { + let parsed; + try { + parsed = JSON.parse(dataNamed); + } catch (e) { + console.error("Invalid data-named-stacks JSON:", e); + parsed = []; + } + const descriptors = parseToDescriptors(parsed); + imageStacks = () => Promise.resolve(descriptors); + } else if (dataImages) { let parsed; try { parsed = JSON.parse(dataImages); @@ -39,6 +111,7 @@ export function mountDicomViewers() { console.error("Invalid data-images JSON:", e); parsed = []; } + // Keep old behavior for data-images if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") { imageStacks = () => Promise.resolve([toWadouri(parsed)]); } else if (Array.isArray(parsed)) {