refactor image stack handling to support richer data formats and improve clarity

This commit is contained in:
Ross
2025-09-15 12:25:57 +01:00
parent c3ea2b4478
commit 8de4efc3e2
2 changed files with 136 additions and 28 deletions
+61 -26
View File
@@ -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<string[][]>;
imageStacks: () => Promise<Array<string[] | { imageIds: string[], name?: string, caseId?: string }>>;
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<StackEntry> = 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];
+75 -2
View File
@@ -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<any[]>;
// 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<any> => {
// 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)) {