many updates
This commit is contained in:
+234
-38
@@ -95,6 +95,7 @@ type StackEntry = {
|
||||
studyInstanceUID?: string;
|
||||
caseId?: string | number;
|
||||
seriesGroups?: StackSeriesGroup[];
|
||||
groupingSource?: "declared" | "derived";
|
||||
};
|
||||
|
||||
const { MouseBindings, KeyboardBindings } = csToolsEnums;
|
||||
@@ -940,14 +941,17 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
? seriesEntry.imageIds
|
||||
: (Array.isArray(seriesEntry?.i) ? seriesEntry.i : []);
|
||||
const normalizedIds = dedupeImageIds(rawIds.map(normalizeToWadouriOrExternal));
|
||||
const rawLabel = typeof seriesEntry?.name === "string" ? seriesEntry.name : "";
|
||||
const rawLabel = typeof seriesEntry?.label === "string"
|
||||
? seriesEntry.label
|
||||
: (typeof seriesEntry?.name === "string" ? seriesEntry.name : "");
|
||||
const label = rawLabel.trim() || `Series ${seriesIdx + 1}`;
|
||||
return {
|
||||
key: String(seriesEntry?.seriesInstanceUID || seriesEntry?.seriesId || `SERIES_${seriesIdx}`),
|
||||
key: String(seriesEntry?.key || seriesEntry?.seriesInstanceUID || seriesEntry?.seriesId || `SERIES_${seriesIdx}`),
|
||||
label,
|
||||
imageIds: normalizedIds,
|
||||
seriesInstanceUID: seriesEntry?.seriesInstanceUID,
|
||||
bValue: Number.isFinite(Number(seriesEntry?.bValue)) ? Number(seriesEntry.bValue) : null,
|
||||
groupValues: seriesEntry?.groupValues,
|
||||
} as StackSeriesGroup;
|
||||
})
|
||||
.filter((group: StackSeriesGroup) => group.imageIds.length > 0);
|
||||
@@ -956,6 +960,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
allStacks.push({
|
||||
imageIds: dedupeImageIds(declaredGroups.flatMap((g) => g.imageIds)),
|
||||
seriesGroups: declaredGroups,
|
||||
groupingSource: "declared",
|
||||
...meta,
|
||||
});
|
||||
continue;
|
||||
@@ -964,7 +969,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
|
||||
const normalizedImageIds = dedupeImageIds(imageIdsRaw.map(normalizeToWadouriOrExternal));
|
||||
if (normalizedImageIds.length > 0) {
|
||||
allStacks.push({ imageIds: normalizedImageIds, seriesGroups: [], ...meta });
|
||||
allStacks.push({ imageIds: normalizedImageIds, seriesGroups: [], groupingSource: "derived", ...meta });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -976,14 +981,23 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
let firstGroups = firstStack.seriesGroups || [];
|
||||
let firstImageIds = firstStack.imageIds;
|
||||
if (firstGroups.length === 0) {
|
||||
firstImageIds = await expandMultiframeImageIds(firstStack.imageIds);
|
||||
await setLoadedImageIdsAndCacheMeta(firstImageIds, { force: true });
|
||||
firstGroups = await deriveSeriesGroupsFromImageIds(firstImageIds);
|
||||
allStacks[0] = { ...firstStack, imageIds: firstImageIds, seriesGroups: firstGroups };
|
||||
setAvailableStacks([...allStacks]);
|
||||
if (autoCacheStack) {
|
||||
appLogger.debug("[init] autoCacheStack true: expanding multiframe and fetching metadata");
|
||||
firstImageIds = await expandMultiframeImageIds(firstStack.imageIds);
|
||||
await setLoadedImageIdsAndCacheMeta(firstImageIds, { force: true });
|
||||
firstGroups = await deriveSeriesGroupsFromImageIds(firstImageIds);
|
||||
allStacks[0] = { ...firstStack, imageIds: firstImageIds, seriesGroups: firstGroups, groupingSource: "derived" };
|
||||
setAvailableStacks([...allStacks]);
|
||||
} else {
|
||||
appLogger.debug("[init] autoCacheStack false: skipping all probes/expansion, relying on server grouping");
|
||||
}
|
||||
}
|
||||
const firstSeries = firstGroups[0]?.imageIds || firstImageIds;
|
||||
await setLoadedImageIdsAndCacheMeta(firstSeries);
|
||||
if (autoCacheStack) {
|
||||
await setLoadedImageIdsAndCacheMeta(firstSeries, { force: true });
|
||||
} else {
|
||||
appLogger.debug("[init] autoCacheStack false: not fetching metadata for firstSeries");
|
||||
}
|
||||
setViewportImageIds(Array(MAX_VIEWPORTS).fill(null).map((_, idx) => (idx === 0 ? (firstSeries || []) : [])));
|
||||
setSeriesGroupsByViewport(Array(MAX_VIEWPORTS).fill(null).map(() => firstGroups));
|
||||
setSelectedSeriesIdxByViewport(Array(MAX_VIEWPORTS).fill(0));
|
||||
@@ -1005,20 +1019,79 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
const handleLoadStack = async (viewportIdx: number, stackIdx: number) => {
|
||||
const stackObj = availableStacks[stackIdx];
|
||||
if (!stackObj) return;
|
||||
// Expand multiframe IDs lazily when the stack is actually loaded.
|
||||
const stack = await expandMultiframeImageIds(stackObj.imageIds || []);
|
||||
const hasDeclaredGroups = stackObj.groupingSource === "declared" && (stackObj.seriesGroups || []).length > 0;
|
||||
if (!autoCacheStack && hasDeclaredGroups && (stackObj.imageIds || []).length > 1) {
|
||||
// Strict lazy mode: never probe, expand, or fetch metadata if grouping is declared and autoCacheStack is false
|
||||
appLogger.debug("[handleLoadStack] autoCacheStack false & declared groups: skipping all probes/expansion, using server grouping only");
|
||||
const stack = dedupeImageIds((stackObj.imageIds || []).map(normalizeToWadouriOrExternal));
|
||||
const seriesGroups = stackObj.seriesGroups || [];
|
||||
setAvailableStacks(prev => {
|
||||
const next = [...prev];
|
||||
if (!next[stackIdx]) return prev;
|
||||
next[stackIdx] = {
|
||||
...next[stackIdx],
|
||||
seriesGroups,
|
||||
imageIds: stack,
|
||||
groupingSource: "declared",
|
||||
};
|
||||
return next;
|
||||
});
|
||||
const primarySeries = seriesGroups[0]?.imageIds || stack;
|
||||
setLoadingState("displayset");
|
||||
try {
|
||||
setViewportModes(prev => {
|
||||
const next = [...prev];
|
||||
next[viewportIdx] = 'stack';
|
||||
return next;
|
||||
});
|
||||
setViewportImageIds(prev => {
|
||||
const next = [...prev];
|
||||
next[viewportIdx] = primarySeries;
|
||||
return next;
|
||||
});
|
||||
setSelectedStackIdx(prev => {
|
||||
const next = [...prev];
|
||||
next[viewportIdx] = stackIdx;
|
||||
return next;
|
||||
});
|
||||
setSeriesGroupsByViewport(prev => {
|
||||
const next = [...prev];
|
||||
next[viewportIdx] = seriesGroups || [];
|
||||
return next;
|
||||
});
|
||||
setSelectedSeriesIdxByViewport(prev => {
|
||||
const next = [...prev];
|
||||
next[viewportIdx] = 0;
|
||||
return next;
|
||||
});
|
||||
appLogger.debug("[handleLoadStack] autoCacheStack false: not fetching metadata for primarySeries");
|
||||
} finally {
|
||||
setLoadingState(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Default: allow expansion/probe if autoCacheStack is true or grouping is not declared
|
||||
const shouldExpandMultiframe = !(hasDeclaredGroups && (stackObj.imageIds || []).length > 1);
|
||||
const stack = shouldExpandMultiframe
|
||||
? await expandMultiframeImageIds(stackObj.imageIds || [])
|
||||
: dedupeImageIds((stackObj.imageIds || []).map(normalizeToWadouriOrExternal));
|
||||
appLogger.debug("[handleLoadStack] Loading stack:", stack.length, "ids. First:", stack[0]?.slice(0, 80));
|
||||
|
||||
await setLoadedImageIdsAndCacheMeta(stack, { force: true });
|
||||
const seriesGroups = await deriveSeriesGroupsFromImageIds(stack);
|
||||
await setLoadedImageIdsAndCacheMeta(stack, { force: Boolean(autoCacheStack) });
|
||||
const seriesGroups = hasDeclaredGroups
|
||||
? (stackObj.seriesGroups || [])
|
||||
: await deriveSeriesGroupsFromImageIds(stack);
|
||||
appLogger.debug("[handleLoadStack] Derived series groups:", seriesGroups.length, "groups. First group has", seriesGroups[0]?.imageIds.length, "images");
|
||||
setAvailableStacks(prev => {
|
||||
const next = [...prev];
|
||||
if (!next[stackIdx]) return prev;
|
||||
next[stackIdx] = { ...next[stackIdx], seriesGroups, imageIds: stack };
|
||||
next[stackIdx] = {
|
||||
...next[stackIdx],
|
||||
seriesGroups,
|
||||
imageIds: stack,
|
||||
groupingSource: hasDeclaredGroups ? "declared" : "derived",
|
||||
};
|
||||
return next;
|
||||
});
|
||||
|
||||
const primarySeries = seriesGroups[0]?.imageIds || stack;
|
||||
appLogger.debug("[handleLoadStack] Setting viewport to primarySeries with", primarySeries.length, "ids. First:", primarySeries[0]?.slice(0, 80));
|
||||
|
||||
@@ -1441,6 +1514,12 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
|
||||
const expandMultiframeImageIds = useCallback(async (rawImageIds: string[]) => {
|
||||
appLogger.debug("[expandMultiframeImageIds] Input:", rawImageIds.length, "imageIds");
|
||||
if (!autoCacheStack) {
|
||||
const passthrough = dedupeImageIds((rawImageIds || []).map(normalizeToWadouriOrExternal));
|
||||
appLogger.debug("[expandMultiframeImageIds] autoCacheStack false: skipping multiframe probe/expansion");
|
||||
appLogger.debug("[expandMultiframeImageIds] Output:", passthrough.length, "total imageIds");
|
||||
return passthrough;
|
||||
}
|
||||
const { singleFrameIds, multiframeGroups } = await splitMultiframeToStacks(rawImageIds);
|
||||
const result = dedupeImageIds([
|
||||
...singleFrameIds,
|
||||
@@ -1448,9 +1527,10 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
]);
|
||||
appLogger.debug("[expandMultiframeImageIds] Output:", result.length, "total imageIds");
|
||||
return result;
|
||||
}, [splitMultiframeToStacks]);
|
||||
}, [autoCacheStack, splitMultiframeToStacks]);
|
||||
|
||||
const getFallbackBValuesForImageId = useCallback(async (imageId: string): Promise<number[]> => {
|
||||
if (!autoCacheStack) return [];
|
||||
if (!imageId.startsWith("wadouri:")) return [];
|
||||
|
||||
const normalized = normalizeToWadouriOrExternal(imageId);
|
||||
@@ -1512,13 +1592,24 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
bValuesByBaseImageRef.current[baseUrl] = [];
|
||||
return [];
|
||||
}
|
||||
}, []);
|
||||
}, [autoCacheStack]);
|
||||
|
||||
const deriveSeriesGroupsFromImageIds = useCallback(async (imageIds: string[]): Promise<StackSeriesGroup[]> => {
|
||||
if (!Array.isArray(imageIds) || imageIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!autoCacheStack) {
|
||||
// Strict lazy mode: do not probe metadata/tags client-side for grouping.
|
||||
return [{
|
||||
key: "SERIES_0",
|
||||
label: "Series 1",
|
||||
imageIds: dedupeImageIds(imageIds),
|
||||
bValue: null,
|
||||
groupValues: {},
|
||||
}];
|
||||
}
|
||||
|
||||
const formatVec = (arr?: number[]) => {
|
||||
if (!Array.isArray(arr) || arr.length === 0) return "";
|
||||
return arr.map((v) => {
|
||||
@@ -1725,6 +1816,42 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
})
|
||||
: [];
|
||||
|
||||
// When metadata is only partially available, avoid splitting by base keys
|
||||
// (series UID/orientation) because missing tags can create phantom groups.
|
||||
const candidatesWithSeriesUid = candidates.filter((c) => !!c.seriesInstanceUID).length;
|
||||
const hasMissingSeriesUid = candidatesWithSeriesUid < candidates.length;
|
||||
const distinctSeriesUidCount = new Set(
|
||||
candidates
|
||||
.map((c) => c.seriesInstanceUID)
|
||||
.filter((value): value is string => !!value)
|
||||
).size;
|
||||
const useSeriesUidInBaseKey = !hasMissingSeriesUid;
|
||||
|
||||
const candidatesWithOrientation = candidates.filter((c) => c.orientationKey !== "ORI_UNKNOWN").length;
|
||||
const hasUnknownOrientation = candidatesWithOrientation < candidates.length;
|
||||
const distinctOrientationCount = new Set(
|
||||
candidates
|
||||
.map((c) => c.orientationKey)
|
||||
.filter((value) => value && value !== "ORI_UNKNOWN")
|
||||
).size;
|
||||
const useOrientationInBaseKey = !hasUnknownOrientation;
|
||||
|
||||
if (
|
||||
(distinctSeriesUidCount > 1 && !useSeriesUidInBaseKey)
|
||||
|| (distinctOrientationCount > 1 && !useOrientationInBaseKey)
|
||||
) {
|
||||
appLogger.debug(
|
||||
"[deriveSeriesGroupsFromImageIds] partial metadata detected; suppressing base-key grouping",
|
||||
{
|
||||
candidates: candidates.length,
|
||||
distinctSeriesUidCount,
|
||||
distinctOrientationCount,
|
||||
hasMissingSeriesUid,
|
||||
hasUnknownOrientation,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const groups = new Map<string, {
|
||||
imageIds: string[];
|
||||
seriesInstanceUID?: string;
|
||||
@@ -1736,8 +1863,8 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
|
||||
candidates.forEach((candidate) => {
|
||||
const baseKey = [
|
||||
candidate.seriesInstanceUID || "NO_UID",
|
||||
candidate.orientationKey,
|
||||
useSeriesUidInBaseKey ? (candidate.seriesInstanceUID || "NO_UID") : "SERIES_ANY",
|
||||
useOrientationInBaseKey ? candidate.orientationKey : "ORI_ANY",
|
||||
].join("|");
|
||||
|
||||
const advancedKey = activeRuleIds
|
||||
@@ -1775,6 +1902,12 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
// If slices repeat uniformly (e.g. 56 images / 28 unique positions),
|
||||
// split by the occurrence index per slice position.
|
||||
if (activeRuleIds.length === 0 && candidates.length >= 4) {
|
||||
const candidatesWithSliceKey = candidates.filter((c) => !!c.sliceKey).length;
|
||||
const sliceKeyCoverage = candidatesWithSliceKey / candidates.length;
|
||||
if (sliceKeyCoverage < 0.9) {
|
||||
return [{ key: "SERIES_0", label: "Series 1", imageIds: dedupeImageIds(imageIds) }];
|
||||
}
|
||||
|
||||
const sliceCounts = new Map<string, number>();
|
||||
candidates.forEach((c) => {
|
||||
if (!c.sliceKey) return;
|
||||
@@ -1859,7 +1992,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
});
|
||||
|
||||
return ordered;
|
||||
}, [advancedGroupingEnabled, getFallbackBValuesForImageId, groupingRuleEnabled]);
|
||||
}, [advancedGroupingEnabled, autoCacheStack, getFallbackBValuesForImageId, groupingRuleEnabled]);
|
||||
|
||||
|
||||
const [viewportGrid, setViewportGrid] = useState({ rows: 1, cols: 1 })
|
||||
@@ -2258,8 +2391,11 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
const recomputeGroups = async () => {
|
||||
const recomputed = await Promise.all(
|
||||
availableStacks.map(async (stack) => {
|
||||
if (stack.groupingSource === "declared" && (stack.seriesGroups || []).length > 0) {
|
||||
return stack;
|
||||
}
|
||||
const seriesGroups = await deriveSeriesGroupsFromImageIds(stack.imageIds);
|
||||
return { ...stack, seriesGroups };
|
||||
return { ...stack, seriesGroups, groupingSource: "derived" as const };
|
||||
})
|
||||
);
|
||||
|
||||
@@ -3055,7 +3191,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
) => {
|
||||
const imageIds = await expandMultiframeImageIds(rawImageIds.map(normalizeToWadouriOrExternal));
|
||||
if (imageIds.length === 0) return;
|
||||
await setLoadedImageIdsAndCacheMeta(imageIds, { force: true });
|
||||
await setLoadedImageIdsAndCacheMeta(imageIds, { force: Boolean(autoCacheStack) });
|
||||
const seriesGroups = await deriveSeriesGroupsFromImageIds(imageIds);
|
||||
const primarySeries = seriesGroups[0]?.imageIds || imageIds;
|
||||
|
||||
@@ -3073,7 +3209,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
return prev;
|
||||
}
|
||||
newStackIdx = prev.length;
|
||||
return [...prev, { imageIds, name, seriesGroups }];
|
||||
return [...prev, { imageIds, name, seriesGroups, groupingSource: "derived" }];
|
||||
});
|
||||
const stackIdxToUse = existingStackIdx !== -1 ? existingStackIdx : newStackIdx;
|
||||
|
||||
@@ -3184,17 +3320,28 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
const stackObj = availableStacks[stackIdx];
|
||||
if (!stackObj) return;
|
||||
let stackImageIds = stackObj.imageIds || [];
|
||||
const hasDeclaredGroups = stackObj.groupingSource === "declared" && (stackObj.seriesGroups || []).length > 0;
|
||||
const shouldExpandMultiframe = !(hasDeclaredGroups && stackImageIds.length > 1);
|
||||
|
||||
// IMPORTANT: Expand multiframe images BEFORE caching metadata
|
||||
const expandedImageIds = await expandMultiframeImageIds(stackImageIds);
|
||||
const expandedImageIds = shouldExpandMultiframe
|
||||
? await expandMultiframeImageIds(stackImageIds)
|
||||
: dedupeImageIds((stackImageIds || []).map(normalizeToWadouriOrExternal));
|
||||
stackImageIds = expandedImageIds;
|
||||
|
||||
await setLoadedImageIdsAndCacheMeta(stackImageIds, { force: true });
|
||||
const seriesGroups = await deriveSeriesGroupsFromImageIds(stackImageIds);
|
||||
await setLoadedImageIdsAndCacheMeta(stackImageIds, { force: Boolean(autoCacheStack) });
|
||||
const seriesGroups = hasDeclaredGroups
|
||||
? (stackObj.seriesGroups || [])
|
||||
: await deriveSeriesGroupsFromImageIds(stackImageIds);
|
||||
setAvailableStacks(prev => {
|
||||
const next = [...prev];
|
||||
if (!next[stackIdx]) return prev;
|
||||
next[stackIdx] = { ...next[stackIdx], seriesGroups, imageIds: stackImageIds };
|
||||
next[stackIdx] = {
|
||||
...next[stackIdx],
|
||||
seriesGroups,
|
||||
imageIds: stackImageIds,
|
||||
groupingSource: hasDeclaredGroups ? "declared" : "derived",
|
||||
};
|
||||
return next;
|
||||
});
|
||||
const primarySeries = seriesGroups[0]?.imageIds || stackImageIds;
|
||||
@@ -4073,7 +4220,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
return next;
|
||||
});
|
||||
|
||||
void setLoadedImageIdsAndCacheMeta(selectedGroup.imageIds, { force: true });
|
||||
void setLoadedImageIdsAndCacheMeta(selectedGroup.imageIds, { force: Boolean(autoCacheStack) });
|
||||
window.setTimeout(() => {
|
||||
const vp = renderingEngineRef.current?.getViewport(`CT_${viewportIdx}`);
|
||||
if (!vp) return;
|
||||
@@ -4244,7 +4391,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
window.loadDicomStackFromWadouriList = async (wadouriImageIds: string[]) => {
|
||||
if (!wadouriImageIds || wadouriImageIds.length === 0) return;
|
||||
const expandedImageIds = await expandMultiframeImageIds(wadouriImageIds);
|
||||
await setLoadedImageIdsAndCacheMeta(expandedImageIds, { force: true });
|
||||
await setLoadedImageIdsAndCacheMeta(expandedImageIds, { force: Boolean(autoCacheStack) });
|
||||
const seriesGroups = await deriveSeriesGroupsFromImageIds(expandedImageIds);
|
||||
const primarySeries = seriesGroups[0]?.imageIds || expandedImageIds;
|
||||
|
||||
@@ -4252,7 +4399,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
let studyInstanceUID: string | undefined = undefined;
|
||||
try {
|
||||
const firstId = expandedImageIds[0];
|
||||
if (firstId && firstId.startsWith("wadouri:")) {
|
||||
if (Boolean(autoCacheStack) && firstId && firstId.startsWith("wadouri:")) {
|
||||
const url = stripWadouriPrefix(firstId);
|
||||
const response = await fetch(url);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
@@ -4264,7 +4411,7 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
}
|
||||
|
||||
setAvailableStacks(prev => {
|
||||
const next = [...prev, { imageIds: expandedImageIds, studyInstanceUID, seriesGroups }];
|
||||
const next = [...prev, { imageIds: expandedImageIds, studyInstanceUID, seriesGroups, groupingSource: "derived" as const }];
|
||||
setViewportImageIds(vpPrev => {
|
||||
const vpNext = [...vpPrev];
|
||||
vpNext[0] = primarySeries;
|
||||
@@ -4530,15 +4677,59 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
window[`truncateStack_${apiKey}`] = truncateStack;
|
||||
window[`getCurrentStackPosition_${apiKey}`] = getCurrentStackPosition;
|
||||
|
||||
window[`loadAdditionalStack_${apiKey}`] = async (imageIds: string[], studyInstanceUID?: string) => {
|
||||
window[`loadAdditionalStack_${apiKey}`] = async (
|
||||
imageIds: string[],
|
||||
studyInstanceUID?: string,
|
||||
declaredSeries?: Array<{
|
||||
key?: string;
|
||||
label?: string;
|
||||
imageIds?: string[];
|
||||
i?: string[];
|
||||
name?: string;
|
||||
seriesInstanceUID?: string;
|
||||
bValue?: number | null;
|
||||
groupValues?: Partial<Record<GroupingRuleId, string>>;
|
||||
}>,
|
||||
) => {
|
||||
if (!Array.isArray(imageIds) || imageIds.length === 0) return;
|
||||
const expandedImageIds = await expandMultiframeImageIds(imageIds);
|
||||
await setLoadedImageIdsAndCacheMeta(expandedImageIds, { force: true });
|
||||
const seriesGroups = await deriveSeriesGroupsFromImageIds(expandedImageIds);
|
||||
const normalizedImageIds = dedupeImageIds((imageIds || []).map(normalizeToWadouriOrExternal));
|
||||
const hasDeclaredSeries = Array.isArray(declaredSeries) && declaredSeries.length > 0;
|
||||
const expandedImageIds = hasDeclaredSeries
|
||||
? normalizedImageIds
|
||||
: await expandMultiframeImageIds(normalizedImageIds);
|
||||
await setLoadedImageIdsAndCacheMeta(expandedImageIds, { force: Boolean(autoCacheStack) });
|
||||
const serverSeriesGroups: StackSeriesGroup[] = Array.isArray(declaredSeries)
|
||||
? declaredSeries
|
||||
.map((entry, idx) => {
|
||||
const rawIds = Array.isArray(entry?.imageIds)
|
||||
? entry.imageIds
|
||||
: (Array.isArray(entry?.i) ? entry.i : []);
|
||||
const normalizedIds = dedupeImageIds(rawIds.map(normalizeToWadouriOrExternal));
|
||||
if (normalizedIds.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
key: String(entry?.key || entry?.seriesInstanceUID || `SERVER_SERIES_${idx}`),
|
||||
label:
|
||||
(typeof entry?.label === "string" && entry.label.trim())
|
||||
|| (typeof entry?.name === "string" && entry.name.trim())
|
||||
|| `Series ${idx + 1}`,
|
||||
imageIds: normalizedIds,
|
||||
seriesInstanceUID: entry?.seriesInstanceUID,
|
||||
bValue: Number.isFinite(Number(entry?.bValue)) ? Number(entry?.bValue) : null,
|
||||
groupValues: entry?.groupValues,
|
||||
} as StackSeriesGroup;
|
||||
})
|
||||
.filter((group): group is StackSeriesGroup => Boolean(group))
|
||||
: [];
|
||||
|
||||
const seriesGroups = serverSeriesGroups.length > 0
|
||||
? serverSeriesGroups
|
||||
: await deriveSeriesGroupsFromImageIds(expandedImageIds);
|
||||
const primarySeries = seriesGroups[0]?.imageIds || expandedImageIds;
|
||||
// Optionally extract StudyInstanceUID if not provided
|
||||
let uid = studyInstanceUID;
|
||||
if (!uid && expandedImageIds[0]?.startsWith("wadouri:")) {
|
||||
if (!uid && Boolean(autoCacheStack) && expandedImageIds[0]?.startsWith("wadouri:")) {
|
||||
try {
|
||||
const url = stripWadouriPrefix(expandedImageIds[0]);
|
||||
const response = await fetch(url);
|
||||
@@ -4549,7 +4740,12 @@ export default function App({ container_id, imageStacks, autoCacheStack, annotat
|
||||
// Ignore errors, fallback to undefined
|
||||
}
|
||||
}
|
||||
setAvailableStacks(prev => [...prev, { imageIds: expandedImageIds, studyInstanceUID: uid, seriesGroups }]);
|
||||
setAvailableStacks(prev => [...prev, {
|
||||
imageIds: expandedImageIds,
|
||||
studyInstanceUID: uid,
|
||||
seriesGroups,
|
||||
groupingSource: serverSeriesGroups.length > 0 ? "declared" : "derived",
|
||||
}]);
|
||||
// Optionally, auto-populate a viewport with the new stack if there is an empty viewport
|
||||
setViewportImageIds(prev => {
|
||||
const next = [...prev];
|
||||
|
||||
@@ -31,6 +31,17 @@ type ViewerDescriptor = {
|
||||
name?: string;
|
||||
caseId?: string;
|
||||
studyId?: string;
|
||||
series?: Array<{
|
||||
key?: string;
|
||||
label?: string;
|
||||
imageIds?: string[];
|
||||
i?: string[];
|
||||
name?: string;
|
||||
seriesId?: string | number;
|
||||
seriesInstanceUID?: string;
|
||||
bValue?: number | null;
|
||||
groupValues?: Record<string, string>;
|
||||
}>;
|
||||
};
|
||||
|
||||
type RootRecord = {
|
||||
@@ -113,6 +124,7 @@ const parseToDescriptors = (parsed: any): Array<ViewerDescriptor> => {
|
||||
name: s.name || s.label || undefined,
|
||||
caseId,
|
||||
studyId: s.studyId || studyId,
|
||||
series: Array.isArray(s.series) ? s.series : undefined,
|
||||
});
|
||||
});
|
||||
return;
|
||||
@@ -130,6 +142,7 @@ const parseToDescriptors = (parsed: any): Array<ViewerDescriptor> => {
|
||||
name: item.name || item.label || undefined,
|
||||
caseId: item.caseId || item.caseUID || undefined,
|
||||
studyId: item.studyId || item.studyUID || item.studyInstanceUID || undefined,
|
||||
series: Array.isArray(item.series) ? item.series : undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user