refactor image stack handling to support richer data formats and improve clarity
This commit is contained in:
+56
-21
@@ -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
|
// Add this type for clarity
|
||||||
declare global {
|
declare global {
|
||||||
@@ -145,7 +151,7 @@ const ALL_MOUSE_BINDINGS = [
|
|||||||
|
|
||||||
type AppProps = {
|
type AppProps = {
|
||||||
container_id?: string;
|
container_id?: string;
|
||||||
imageStacks: () => Promise<string[][]>;
|
imageStacks: () => Promise<Array<string[] | { imageIds: string[], name?: string, caseId?: string }>>;
|
||||||
autoCacheStack?: boolean; // <-- new prop
|
autoCacheStack?: boolean; // <-- new prop
|
||||||
annotationJson?: string;
|
annotationJson?: string;
|
||||||
viewerState?: string;
|
viewerState?: string;
|
||||||
@@ -252,32 +258,61 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
|||||||
|
|
||||||
// Load available stacks on mount
|
// Load available stacks on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
imageStacks().then(async stacks => {
|
let mounted = true;
|
||||||
// Try to extract StudyInstanceUID for each stack
|
|
||||||
const stackEntries: StackEntry[] = await Promise.all(
|
imageStacks()
|
||||||
stacks.map(async imageIds => {
|
.then(async (descriptors) => {
|
||||||
let studyInstanceUID: string | undefined = undefined;
|
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[] };
|
||||||
|
}
|
||||||
|
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 {
|
try {
|
||||||
// Try to fetch the first image as a blob and parse it
|
const first = imageIds[0];
|
||||||
const firstId = imageIds[0];
|
if (typeof fetch === 'function' && typeof (window as any).dicomParser !== 'undefined' && first.startsWith('wadouri:')) {
|
||||||
if (firstId && firstId.startsWith("wadouri:")) {
|
const url = first.slice(7);
|
||||||
const url = firstId.slice(8);
|
const resp = await fetch(url);
|
||||||
const response = await fetch(url);
|
const buffer = await resp.arrayBuffer();
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
const ds = (window as any).dicomParser.parseDicom(new Uint8Array(buffer));
|
||||||
const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
studyInstanceUID = ds.string('x0020000d');
|
||||||
studyInstanceUID = dataSet.string('x0020000d');
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore errors, fallback to undefined
|
// ignore UID extraction failures
|
||||||
}
|
}
|
||||||
return { imageIds, studyInstanceUID };
|
}
|
||||||
|
return {
|
||||||
|
imageIds,
|
||||||
|
studyInstanceUID,
|
||||||
|
name: (d as any).name,
|
||||||
|
caseId: (d as any).caseId,
|
||||||
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
setAvailableStacks(stackEntries);
|
|
||||||
//setAvailableStacks(stacks.map(imageIds => ({ imageIds, studyInstanceUID: undefined })));
|
setAvailableStacks(normalized);
|
||||||
await setLoadedImageIdsAndCacheMeta(stacks[0]);
|
|
||||||
setViewportImageIds(Array(MAX_VIEWPORTS).fill(stacks[0] || []));
|
// 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 handleLoadStack = async (viewportIdx: number, stackIdx: number) => {
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ import { createImageIds, availableImageIds } from "./lib/createImageIds.ts"
|
|||||||
const toWadouri = (arr: string[]) =>
|
const toWadouri = (arr: string[]) =>
|
||||||
arr.map(url => url.startsWith("wadouri:") ? url : `wadouri:${url}`);
|
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() {
|
export function mountDicomViewers() {
|
||||||
// Test containers
|
// Test containers
|
||||||
const test_containers = document.querySelectorAll(".dicom-viewer-test-root");
|
const test_containers = document.querySelectorAll(".dicom-viewer-test-root");
|
||||||
@@ -28,10 +34,76 @@ export function mountDicomViewers() {
|
|||||||
const containers = document.querySelectorAll(".dicom-viewer-root");
|
const containers = document.querySelectorAll(".dicom-viewer-root");
|
||||||
containers.forEach((container) => {
|
containers.forEach((container) => {
|
||||||
const id = container.id || '';
|
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');
|
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;
|
let parsed;
|
||||||
try {
|
try {
|
||||||
parsed = JSON.parse(dataImages);
|
parsed = JSON.parse(dataImages);
|
||||||
@@ -39,6 +111,7 @@ export function mountDicomViewers() {
|
|||||||
console.error("Invalid data-images JSON:", e);
|
console.error("Invalid data-images JSON:", e);
|
||||||
parsed = [];
|
parsed = [];
|
||||||
}
|
}
|
||||||
|
// Keep old behavior for data-images
|
||||||
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") {
|
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") {
|
||||||
imageStacks = () => Promise.resolve([toWadouri(parsed)]);
|
imageStacks = () => Promise.resolve([toWadouri(parsed)]);
|
||||||
} else if (Array.isArray(parsed)) {
|
} else if (Array.isArray(parsed)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user