feat(logging): implement structured logging with configurable options

- Added a logger utility to manage logging levels and formats.
- Integrated logging into main application and non-DICOM test components.
- Enhanced error handling with logging for invalid JSON data.
- Introduced global functions to set and get logging configurations.
- Updated DICOM viewer mounting logic to support logging configurations.
This commit is contained in:
Ross
2026-05-19 09:41:52 +01:00
parent 77bbbd548e
commit 4dd63668b0
4 changed files with 406 additions and 128 deletions
+166 -89
View File
@@ -18,6 +18,7 @@ import { volumeLoader } from '@cornerstonejs/core';
import { metaData } from '@cornerstonejs/core'; import { metaData } from '@cornerstonejs/core';
import * as dicomParser from "dicom-parser"; import * as dicomParser from "dicom-parser";
import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader" import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader"
import { getLoggerConfig, logger, parseLogFormat, setLoggerConfig, type LogFormat } from "./lib/logger";
import { NoLabelArrowAnnotateTool } from "./NoLabelArrowAnnotateTool"; import { NoLabelArrowAnnotateTool } from "./NoLabelArrowAnnotateTool";
import { render } from "@cornerstonejs/tools/tools/displayTools/Labelmap/labelmapDisplay" import { render } from "@cornerstonejs/tools/tools/displayTools/Labelmap/labelmapDisplay"
@@ -171,6 +172,13 @@ const TOOL_OPTIONS = [
const THUMB_CAPTURE_MAX_RETRIES = 3; const THUMB_CAPTURE_MAX_RETRIES = 3;
const THUMB_CAPTURE_RETRY_BASE_MS = 220; const THUMB_CAPTURE_RETRY_BASE_MS = 220;
const LOG_SETTINGS_KEY = "dv3d_logging";
const LOG_FORMAT_OPTIONS: Array<{ label: string; value: LogFormat }> = [
{ label: "Compact", value: "compact" },
{ label: "Verbose", value: "verbose" },
{ label: "JSON", value: "json" },
];
const appLogger = logger.child("app");
function getToolIcon(toolName?: string) { function getToolIcon(toolName?: string) {
if (!toolName) return '🔖'; if (!toolName) return '🔖';
@@ -301,11 +309,15 @@ type AppProps = {
autoCacheStack?: boolean; // <-- new prop autoCacheStack?: boolean; // <-- new prop
annotationJson?: string; annotationJson?: string;
viewerState?: string; viewerState?: string;
initialLoggerConfig?: {
debug?: boolean;
format?: LogFormat;
};
// ...other props if needed // ...other props if needed
}; };
// App definintion // App definintion
function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewerState, ...props }: AppProps) { function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewerState, initialLoggerConfig }: AppProps) {
const appRootRef = useRef<HTMLDivElement>(null); const appRootRef = useRef<HTMLDivElement>(null);
const elementRef = useRef<HTMLDivElement>(null) const elementRef = useRef<HTMLDivElement>(null)
const running = useRef(false) const running = useRef(false)
@@ -313,6 +325,10 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const MOUSE_SETTINGS_KEY = "dv3d_mouseToolBindings"; const MOUSE_SETTINGS_KEY = "dv3d_mouseToolBindings";
const CTRL_MOUSE_SETTINGS_KEY = "dv3d_ctrlMouseToolBindings"; const CTRL_MOUSE_SETTINGS_KEY = "dv3d_ctrlMouseToolBindings";
const initialLogDebug = !!initialLoggerConfig?.debug;
const initialLogFormat = parseLogFormat(initialLoggerConfig?.format ?? getLoggerConfig().format);
const [loggingDebugEnabled, setLoggingDebugEnabled] = useState(initialLogDebug);
const [loggingFormat, setLoggingFormat] = useState<LogFormat>(initialLogFormat);
const [settingsOpen, setSettingsOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false);
// Settings open state // Settings open state
@@ -369,11 +385,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
// If called with a function, log the result // If called with a function, log the result
_setViewportImageIds(prev => { _setViewportImageIds(prev => {
const next = (value as (prev: string[][]) => string[][])(prev); const next = (value as (prev: string[][]) => string[][])(prev);
console.log("setViewportImageIds (fn):", next); appLogger.debug("setViewportImageIds (fn):", next);
return next; return next;
}); });
} else { } else {
console.log("setViewportImageIds:", value); appLogger.debug("setViewportImageIds:", value);
_setViewportImageIds(value); _setViewportImageIds(value);
} }
}; };
@@ -686,7 +702,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
msg.toLowerCase().includes('webgl') || msg.toLowerCase().includes('webgl') ||
msg.toLowerCase().includes('colormap') msg.toLowerCase().includes('colormap')
) { ) {
console.error('Detected graphics/rendering error:', ev); appLogger.error('Detected graphics/rendering error:', ev);
setGraphicsErrorMessage(msg || null); setGraphicsErrorMessage(msg || null);
setGraphicsErrorVisible(true); setGraphicsErrorVisible(true);
} }
@@ -708,7 +724,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
text.toLowerCase().includes('webgl') || text.toLowerCase().includes('webgl') ||
text.toLowerCase().includes('colormap') text.toLowerCase().includes('colormap')
) { ) {
console.error('Detected graphics/rendering rejection:', ev); appLogger.error('Detected graphics/rendering rejection:', ev);
setGraphicsErrorMessage(text || null); setGraphicsErrorMessage(text || null);
setGraphicsErrorVisible(true); setGraphicsErrorVisible(true);
} }
@@ -741,7 +757,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
fn(annotationJson); fn(annotationJson);
} }
} catch (e) { } catch (e) {
console.error("Failed to import annotations from prop:", e); appLogger.error("Failed to import annotations from prop:", e);
} }
} }
if (viewerState) { if (viewerState) {
@@ -751,7 +767,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
fn(viewerState); fn(viewerState);
} }
} catch (e) { } catch (e) {
console.error("Failed to import viewer state from prop:", e); appLogger.error("Failed to import viewer state from prop:", e);
} }
} }
setLoadingState(null); setLoadingState(null);
@@ -865,7 +881,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
} }
}) })
.catch(err => { .catch(err => {
console.error("Failed to load imageStacks:", err); appLogger.error("Failed to load imageStacks:", err);
setAvailableStacks([]); setAvailableStacks([]);
setViewportImageIds(Array(MAX_VIEWPORTS).fill([])); setViewportImageIds(Array(MAX_VIEWPORTS).fill([]));
}); });
@@ -877,16 +893,16 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const stackObj = availableStacks[stackIdx]; const stackObj = availableStacks[stackIdx];
if (!stackObj) return; if (!stackObj) return;
let stack = stackObj.imageIds; let stack = stackObj.imageIds;
console.log("[handleLoadStack] Starting with imageIds:", stack.length, "ids. First:", stack[0]?.slice(0, 80)); appLogger.debug("[handleLoadStack] Starting with imageIds:", stack.length, "ids. First:", stack[0]?.slice(0, 80));
// IMPORTANT: Expand multiframe images BEFORE caching metadata // IMPORTANT: Expand multiframe images BEFORE caching metadata
const expandedStack = await expandMultiframeImageIds(stack); const expandedStack = await expandMultiframeImageIds(stack);
console.log("[handleLoadStack] After expansion:", expandedStack.length, "ids. First:", expandedStack[0]?.slice(0, 80)); appLogger.debug("[handleLoadStack] After expansion:", expandedStack.length, "ids. First:", expandedStack[0]?.slice(0, 80));
stack = expandedStack; stack = expandedStack;
await setLoadedImageIdsAndCacheMeta(stack, { force: true }); await setLoadedImageIdsAndCacheMeta(stack, { force: true });
const seriesGroups = await deriveSeriesGroupsFromImageIds(stack); const seriesGroups = await deriveSeriesGroupsFromImageIds(stack);
console.log("[handleLoadStack] Derived series groups:", seriesGroups.length, "groups. First group has", seriesGroups[0]?.imageIds.length, "images"); appLogger.debug("[handleLoadStack] Derived series groups:", seriesGroups.length, "groups. First group has", seriesGroups[0]?.imageIds.length, "images");
setAvailableStacks(prev => { setAvailableStacks(prev => {
const next = [...prev]; const next = [...prev];
if (!next[stackIdx]) return prev; if (!next[stackIdx]) return prev;
@@ -895,7 +911,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
}); });
const primarySeries = seriesGroups[0]?.imageIds || stack; const primarySeries = seriesGroups[0]?.imageIds || stack;
console.log("[handleLoadStack] Setting viewport to primarySeries with", primarySeries.length, "ids. First:", primarySeries[0]?.slice(0, 80)); appLogger.debug("[handleLoadStack] Setting viewport to primarySeries with", primarySeries.length, "ids. First:", primarySeries[0]?.slice(0, 80));
setLoadingState("displayset"); setLoadingState("displayset");
try { try {
@@ -941,7 +957,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const [showAdvancedCtrlMouseBindings, setShowAdvancedCtrlMouseBindings] = useState(false); const [showAdvancedCtrlMouseBindings, setShowAdvancedCtrlMouseBindings] = useState(false);
const sortViewportImageIdsBySliceLocation = () => { const sortViewportImageIdsBySliceLocation = () => {
console.log("Sorting viewport imageIds by slice location"); appLogger.debug("Sorting viewport imageIds by slice location");
setViewportImageIds(prev => { setViewportImageIds(prev => {
const next = prev.map((imageIds, idx) => { const next = prev.map((imageIds, idx) => {
if (!imageIds || imageIds.length === 0) return imageIds; if (!imageIds || imageIds.length === 0) return imageIds;
@@ -1160,8 +1176,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
// Wrap setLoadedImageIds to also cache DICOM metadata // Wrap setLoadedImageIds to also cache DICOM metadata
const setLoadedImageIdsAndCacheMeta = async (imageIds: string[], options?: { force?: boolean }) => { const setLoadedImageIdsAndCacheMeta = async (imageIds: string[], options?: { force?: boolean }) => {
console.log("Setting loaded imageIds:", imageIds); appLogger.debug("Setting loaded imageIds:", imageIds);
console.log("autoCacheStack:", autoCacheStack); appLogger.debug("autoCacheStack:", autoCacheStack);
const loadPromises: Promise<any>[] = []; const loadPromises: Promise<any>[] = [];
const shouldCache = Boolean(autoCacheStack || options?.force); const shouldCache = Boolean(autoCacheStack || options?.force);
@@ -1175,7 +1191,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
urlsToLoad.add(baseUrl); urlsToLoad.add(baseUrl);
} }
} catch (e) { } catch (e) {
console.warn(`Failed to cache metadata for ${imageId}:`, e); appLogger.warn(`Failed to cache metadata for ${imageId}:`, e);
} }
} }
} }
@@ -1188,7 +1204,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
loadPromises.push(result); loadPromises.push(result);
} }
} else { } else {
console.warn("MetaDataManager not found. Unable to cache metadata."); appLogger.warn("MetaDataManager not found. Unable to cache metadata.");
} }
} }
@@ -1197,7 +1213,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
// Trigger sort if enabled // Trigger sort if enabled
if (orderBySliceLocation) { if (orderBySliceLocation) {
console.log("Sorting viewport imageIds by slice location"); appLogger.debug("Sorting viewport imageIds by slice location");
sortViewportImageIdsBySliceLocation(); sortViewportImageIdsBySliceLocation();
} }
} }
@@ -1213,7 +1229,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const cacheKey = `wadouri:${url}`; const cacheKey = `wadouri:${url}`;
const cached = multiframeCountCacheRef.current[cacheKey]; const cached = multiframeCountCacheRef.current[cacheKey];
if (cached !== undefined) { if (cached !== undefined) {
console.log("[getNumberOfFramesForImageId] Cache hit:", cacheKey.slice(0, 60), "->", cached); appLogger.debug("[getNumberOfFramesForImageId] Cache hit:", cacheKey.slice(0, 60), "->", cached);
return cached; return cached;
} }
@@ -1227,7 +1243,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const multiframeMeta = metaData.get("multiframeModule", normalized) as any; const multiframeMeta = metaData.get("multiframeModule", normalized) as any;
const fromMeta = Number(multiframeMeta?.numberOfFrames); const fromMeta = Number(multiframeMeta?.numberOfFrames);
if (Number.isFinite(fromMeta) && fromMeta > 1) { if (Number.isFinite(fromMeta) && fromMeta > 1) {
console.log("[getNumberOfFramesForImageId] Found in metadata:", fromMeta); appLogger.debug("[getNumberOfFramesForImageId] Found in metadata:", fromMeta);
multiframeCountCacheRef.current[cacheKey] = Math.floor(fromMeta); multiframeCountCacheRef.current[cacheKey] = Math.floor(fromMeta);
return Math.floor(fromMeta); return Math.floor(fromMeta);
} }
@@ -1240,7 +1256,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
if (cacheManager?.get) { if (cacheManager?.get) {
const dataSet = cacheManager.get(url); const dataSet = cacheManager.get(url);
const fromCache = readFramesFromDataSet(dataSet); const fromCache = readFramesFromDataSet(dataSet);
console.log("[getNumberOfFramesForImageId] Found in cache manager:", fromCache); appLogger.debug("[getNumberOfFramesForImageId] Found in cache manager:", fromCache);
multiframeCountCacheRef.current[cacheKey] = fromCache; multiframeCountCacheRef.current[cacheKey] = fromCache;
return fromCache; return fromCache;
} }
@@ -1249,16 +1265,16 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
} }
try { try {
console.log("[getNumberOfFramesForImageId] Fetching:", url.slice(0, 80)); appLogger.debug("[getNumberOfFramesForImageId] Fetching:", url.slice(0, 80));
const response = await fetch(url); const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer(); const arrayBuffer = await response.arrayBuffer();
const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer)); const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
const fromFetch = readFramesFromDataSet(dataSet); const fromFetch = readFramesFromDataSet(dataSet);
console.log("[getNumberOfFramesForImageId] Found via fetch:", fromFetch); appLogger.debug("[getNumberOfFramesForImageId] Found via fetch:", fromFetch);
multiframeCountCacheRef.current[cacheKey] = fromFetch; multiframeCountCacheRef.current[cacheKey] = fromFetch;
return fromFetch; return fromFetch;
} catch (e) { } catch (e) {
console.log("[getNumberOfFramesForImageId] Fetch failed, returning 1:", e); appLogger.debug("[getNumberOfFramesForImageId] Fetch failed, returning 1:", e);
multiframeCountCacheRef.current[cacheKey] = 1; multiframeCountCacheRef.current[cacheKey] = 1;
return 1; return 1;
} }
@@ -1266,7 +1282,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const expandMultiframeImageIds = useCallback(async (rawImageIds: string[]) => { const expandMultiframeImageIds = useCallback(async (rawImageIds: string[]) => {
const expanded: string[] = []; const expanded: string[] = [];
console.log("[expandMultiframeImageIds] Input:", rawImageIds.length, "imageIds"); appLogger.debug("[expandMultiframeImageIds] Input:", rawImageIds.length, "imageIds");
for (const raw of rawImageIds || []) { for (const raw of rawImageIds || []) {
const imageId = normalizeToWadouriOrExternal(raw); const imageId = normalizeToWadouriOrExternal(raw);
@@ -1276,7 +1292,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
} }
const frameCount = await getNumberOfFramesForImageId(imageId); const frameCount = await getNumberOfFramesForImageId(imageId);
console.log("[expandMultiframeImageIds] frameCount for", imageId.slice(0, 80), ":", frameCount); appLogger.debug("[expandMultiframeImageIds] frameCount for", imageId.slice(0, 80), ":", frameCount);
if (frameCount <= 1) { if (frameCount <= 1) {
expanded.push(imageId); expanded.push(imageId);
continue; continue;
@@ -1285,11 +1301,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
for (let frame = 1; frame <= frameCount; frame++) { for (let frame = 1; frame <= frameCount; frame++) {
expanded.push(withFrameQuery(imageId, frame)); expanded.push(withFrameQuery(imageId, frame));
} }
console.log("[expandMultiframeImageIds] Expanded to", frameCount, "frames"); appLogger.debug("[expandMultiframeImageIds] Expanded to", frameCount, "frames");
} }
const result = dedupeImageIds(expanded); const result = dedupeImageIds(expanded);
console.log("[expandMultiframeImageIds] Output:", result.length, "total imageIds"); appLogger.debug("[expandMultiframeImageIds] Output:", result.length, "total imageIds");
return result; return result;
}, [getNumberOfFramesForImageId]); }, [getNumberOfFramesForImageId]);
@@ -1689,6 +1705,17 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const ctrl = localStorage.getItem(CTRL_MOUSE_SETTINGS_KEY); const ctrl = localStorage.getItem(CTRL_MOUSE_SETTINGS_KEY);
if (mouse) setMouseToolBindings(JSON.parse(mouse)); if (mouse) setMouseToolBindings(JSON.parse(mouse));
if (ctrl) setCtrlMouseToolBindings(JSON.parse(ctrl)); if (ctrl) setCtrlMouseToolBindings(JSON.parse(ctrl));
const rawLogging = localStorage.getItem(LOG_SETTINGS_KEY);
if (rawLogging) {
const parsed = JSON.parse(rawLogging);
if (typeof parsed?.debug === "boolean") {
setLoggingDebugEnabled(parsed.debug);
}
if (typeof parsed?.format === "string") {
setLoggingFormat(parseLogFormat(parsed.format));
}
}
} catch (e) { } catch (e) {
// Ignore parse errors // Ignore parse errors
} }
@@ -1707,6 +1734,24 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
} catch (e) { } } catch (e) { }
}, [ctrlMouseToolBindings]); }, [ctrlMouseToolBindings]);
useEffect(() => {
setLoggerConfig({
debug: loggingDebugEnabled,
format: loggingFormat,
});
try {
localStorage.setItem(
LOG_SETTINGS_KEY,
JSON.stringify({
debug: loggingDebugEnabled,
format: loggingFormat,
})
);
} catch (e) {
// Ignore storage access errors
}
}, [loggingDebugEnabled, loggingFormat]);
useEffect(() => { useEffect(() => {
window.loadDicomStackFromFiles = (files: FileList | File[]) => { window.loadDicomStackFromFiles = (files: FileList | File[]) => {
const fileArray = Array.from(files); const fileArray = Array.from(files);
@@ -1791,7 +1836,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
visibleStackImageIds.forEach((ids) => { visibleStackImageIds.forEach((ids) => {
setLoadedImageIdsAndCacheMeta(ids, { force: true }).catch((err) => { setLoadedImageIdsAndCacheMeta(ids, { force: true }).catch((err) => {
console.warn('Reference cursor metadata warm-up failed:', err); appLogger.warn('Reference cursor metadata warm-up failed:', err);
}); });
}); });
@@ -2000,7 +2045,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
} }
} catch (e) { } catch (e) {
// ignore individual entry errors // ignore individual entry errors
console.debug('collectFilesFromDirectoryHandle: skipping entry due to error', e); appLogger.debug('collectFilesFromDirectoryHandle: skipping entry due to error', e);
} }
} }
return files; return files;
@@ -2020,7 +2065,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
} }
return; return;
} catch (e) { } catch (e) {
console.warn('showDirectoryPicker failed or cancelled', e); appLogger.warn('showDirectoryPicker failed or cancelled', e);
// fallthrough to input click // fallthrough to input click
} }
} }
@@ -2081,7 +2126,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
for (const entry of parsedEntries) { for (const entry of parsedEntries) {
// Skip files that are neither images nor successfully parsed DICOMs // Skip files that are neither images nor successfully parsed DICOMs
if (!entry.isImage && !entry.parsedSuccessfully) { if (!entry.isImage && !entry.parsedSuccessfully) {
console.debug('Skipping non-DICOM/non-image file:', entry.file.name); appLogger.debug('Skipping non-DICOM/non-image file:', entry.file.name);
continue; continue;
} }
const key = entry.seriesInstanceUID || entry.studyInstanceUID || (entry.file.webkitRelativePath ? entry.file.webkitRelativePath.split('/')[0] : entry.file.name); const key = entry.seriesInstanceUID || entry.studyInstanceUID || (entry.file.webkitRelativePath ? entry.file.webkitRelativePath.split('/')[0] : entry.file.name);
@@ -2245,19 +2290,19 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const currentImageId = imageIds[currentIndex]; const currentImageId = imageIds[currentIndex];
if (currentImageId) { if (currentImageId) {
console.log("[updateOverlay] Current image:", currentImageId.slice(0, 80), "frameIndex status:", frameIndex, "totalFrames:", totalFrames); appLogger.debug("[updateOverlay] Current image:", currentImageId.slice(0, 80), "frameIndex status:", frameIndex, "totalFrames:", totalFrames);
// Detect frame index from imageId query // Detect frame index from imageId query
if (currentImageId.includes('&frame=')) { if (currentImageId.includes('&frame=')) {
const match = currentImageId.match(/&frame=(\d+)/); const match = currentImageId.match(/&frame=(\d+)/);
if (match) { if (match) {
frameIndex = parseInt(match[1], 10); frameIndex = parseInt(match[1], 10);
console.log("[updateOverlay] Frame detected:", frameIndex); appLogger.debug("[updateOverlay] Frame detected:", frameIndex);
// Try to get total frames from metadata // Try to get total frames from metadata
const baseImageId = currentImageId.split('&frame=')[0]; const baseImageId = currentImageId.split('&frame=')[0];
const multiframeMeta = metaData.get("multiframeModule", baseImageId) as any; const multiframeMeta = metaData.get("multiframeModule", baseImageId) as any;
if (multiframeMeta?.numberOfFrames) { if (multiframeMeta?.numberOfFrames) {
totalFrames = multiframeMeta.numberOfFrames; totalFrames = multiframeMeta.numberOfFrames;
console.log("[updateOverlay] Total frames from metadata:", totalFrames); appLogger.debug("[updateOverlay] Total frames from metadata:", totalFrames);
} else { } else {
// Fallback: count unique frame indices in imageIds // Fallback: count unique frame indices in imageIds
const frameNumbers = imageIds.map(id => { const frameNumbers = imageIds.map(id => {
@@ -2266,12 +2311,12 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
}).filter(n => n !== null && typeof n === 'number'); }).filter(n => n !== null && typeof n === 'number');
if (frameNumbers.length > 0) { if (frameNumbers.length > 0) {
totalFrames = Math.max(...frameNumbers as number[]); totalFrames = Math.max(...frameNumbers as number[]);
console.log("[updateOverlay] Total frames from imageIds:", totalFrames); appLogger.debug("[updateOverlay] Total frames from imageIds:", totalFrames);
} }
} }
} }
} else { } else {
console.log("[updateOverlay] No frame query in imageId. imageIds.length:", imageIds.length); appLogger.debug("[updateOverlay] No frame query in imageId. imageIds.length:", imageIds.length);
} }
const seriesMeta = metaData.get("generalSeriesModule", currentImageId) as any; const seriesMeta = metaData.get("generalSeriesModule", currentImageId) as any;
@@ -2692,7 +2737,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
useEffect(() => { useEffect(() => {
const setup = async () => { const setup = async () => {
console.log("setup") appLogger.debug("setup")
// 1. Initialize Cornerstone3D and tools ONCE // 1. Initialize Cornerstone3D and tools ONCE
if (!running.current) { if (!running.current) {
@@ -2750,7 +2795,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
return { promise }; return { promise };
}); });
} catch (e) { } catch (e) {
console.warn('Failed to register external image loader', e); appLogger.warn('Failed to register external image loader', e);
} }
} }
@@ -2881,13 +2926,13 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
// Do not attach DICOM-specific tools to external images // Do not attach DICOM-specific tools to external images
mainToolGroup.removeViewports(renderingEngineId, viewportId); mainToolGroup.removeViewports(renderingEngineId, viewportId);
} catch (err) { } catch (err) {
console.warn('Failed to render external image in viewport', err); appLogger.warn('Failed to render external image in viewport', err);
} }
} else { } else {
try { try {
viewport.setStack(imageIds); viewport.setStack(imageIds);
} catch (err) { } catch (err) {
console.warn('viewport.setStack failed', err); appLogger.warn('viewport.setStack failed', err);
} }
} }
} }
@@ -2962,7 +3007,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
propsToRestore.colormap = colormap; propsToRestore.colormap = colormap;
} }
setTimeout(() => { setTimeout(() => {
console.log("Restoring properties (timeout) for viewport", i, propsToRestore); appLogger.debug("Restoring properties (timeout) for viewport", i, propsToRestore);
viewport.setProperties(propsToRestore); viewport.setProperties(propsToRestore);
viewport.render() viewport.render()
}, 100); }, 100);
@@ -3056,7 +3101,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
if (viewport && typeof viewport.resetCamera === "function") { if (viewport && typeof viewport.resetCamera === "function") {
viewport.resetCamera(); viewport.resetCamera();
console.log("Restoring viewport properties for viewport", viewportIdx); appLogger.debug("Restoring viewport properties for viewport", viewportIdx);
viewport.resetProperties(); // Resets window/level and other properties viewport.resetProperties(); // Resets window/level and other properties
viewport.render(); viewport.render();
} }
@@ -3070,7 +3115,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
if (renderingEngine) { if (renderingEngine) {
const viewport = renderingEngine.getViewport(viewportId); const viewport = renderingEngine.getViewport(viewportId);
if (viewport && typeof viewport.setProperties === "function") { if (viewport && typeof viewport.setProperties === "function") {
console.log(`Applying preset to viewport ${viewportIdx}: center=${center}, width=${width}`); appLogger.debug(`Applying preset to viewport ${viewportIdx}: center=${center}, width=${width}`);
viewport.setProperties({ viewport.setProperties({
voiRange: { voiRange: {
lower: center - width / 2, lower: center - width / 2,
@@ -3140,7 +3185,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
updateOverlay(viewportIdx, viewport); updateOverlay(viewportIdx, viewport);
return true; return true;
} catch (err) { } catch (err) {
console.warn("Failed to apply default window level", err); appLogger.warn("Failed to apply default window level", err);
return false; return false;
} }
}, [updateOverlay, viewportImageIds]); }, [updateOverlay, viewportImageIds]);
@@ -3708,8 +3753,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
function truncateStack(lower: number, upper: number) { function truncateStack(lower: number, upper: number) {
// Only allow when 1 viewport and 1 stack is loaded // Only allow when 1 viewport and 1 stack is loaded
console.log("Truncating stack from", lower, "to", upper); appLogger.debug("Truncating stack from", lower, "to", upper);
console.log(stackOrderModified) appLogger.debug(stackOrderModified)
if ( if (
viewportGrid.rows !== 1 || viewportGrid.rows !== 1 ||
viewportGrid.cols !== 1 || viewportGrid.cols !== 1 ||
@@ -3880,8 +3925,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
u: s.studyInstanceUID, // studyInstanceUID u: s.studyInstanceUID, // studyInstanceUID
g: s.seriesGroups, g: s.seriesGroups,
})); }));
console.log("Exporting viewer state with", numActive, "active viewports"); appLogger.debug("Exporting viewer state with", numActive, "active viewports");
console.log("modes", modes); appLogger.debug("modes", modes);
// Only keep minimal properties // Only keep minimal properties
const renderingEngine = renderingEngineRef.current; const renderingEngine = renderingEngineRef.current;
@@ -3892,9 +3937,9 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const volumeSliceIndices: (number | null)[] = []; const volumeSliceIndices: (number | null)[] = [];
for (let i = 0; i < numActive; i++) { for (let i = 0; i < numActive; i++) {
let p = null, s = null, c = null, o = null, vIdx = null; let p = null, s = null, c = null, o = null, vIdx = null;
console.log("Exporting state for viewport", i); appLogger.debug("Exporting state for viewport", i);
if (renderingEngine) { if (renderingEngine) {
console.log("Rendering engine found, exporting state for viewport", i); appLogger.debug("Rendering engine found, exporting state for viewport", i);
const viewportId = `CT_${i}`; const viewportId = `CT_${i}`;
const viewport = renderingEngine.getViewport(viewportId); const viewport = renderingEngine.getViewport(viewportId);
if (viewport) { if (viewport) {
@@ -3908,11 +3953,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
if (typeof viewport.getCamera === "function") { if (typeof viewport.getCamera === "function") {
c = viewport.getCamera(); c = viewport.getCamera();
} }
console.log("viewport", viewportId, "properties", p); appLogger.debug("viewport", viewportId, "properties", p);
// --- Volume orientation and slice index --- // --- Volume orientation and slice index ---
if (modes[i] === "volume") { if (modes[i] === "volume") {
console.log("Getting orientation for viewport", i); appLogger.debug("Getting orientation for viewport", i);
console.log("viewport.viewportProperties", viewport.viewportProperties); appLogger.debug("viewport.viewportProperties", viewport.viewportProperties);
o = viewport.viewportProperties?.orientation || null; o = viewport.viewportProperties?.orientation || null;
if (typeof viewport.getSliceIndex === "function") { if (typeof viewport.getSliceIndex === "function") {
vIdx = viewport.getSliceIndex(); vIdx = viewport.getSliceIndex();
@@ -3985,7 +4030,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const numActive = state.grid?.rows * state.grid?.cols || 1; const numActive = state.grid?.rows * state.grid?.cols || 1;
let allReady = true; let allReady = true;
for (let i = 0; i < numActive; i++) { for (let i = 0; i < numActive; i++) {
console.log("Checking viewport", i); appLogger.debug("Checking viewport", i);
const viewport = renderingEngine.getViewport(`CT_${i}`); const viewport = renderingEngine.getViewport(`CT_${i}`);
if (!viewport) { allReady = false; break; } if (!viewport) { allReady = false; break; }
const stackIdx = state.stackIdx?.[i]; const stackIdx = state.stackIdx?.[i];
@@ -3998,7 +4043,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
viewportImageIds.length !== importedImageIds.length || viewportImageIds.length !== importedImageIds.length ||
!viewportImageIds.every((id, idx) => id === importedImageIds[idx]) !viewportImageIds.every((id, idx) => id === importedImageIds[idx])
) { ) {
console.warn( appLogger.warn(
`Viewport ${i}: importedImageIds do not match viewportImageIds`, `Viewport ${i}: importedImageIds do not match viewportImageIds`,
{ importedImageIds, viewportImageIds } { importedImageIds, viewportImageIds }
); );
@@ -4018,7 +4063,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
// First pass: set stacks/volumes and stack positions so viewports have image resources // First pass: set stacks/volumes and stack positions so viewports have image resources
for (let i = 0; i < numActive; i++) { for (let i = 0; i < numActive; i++) {
console.log("Setting stack/volume for viewport", i); appLogger.debug("Setting stack/volume for viewport", i);
const viewport = renderingEngine.getViewport(`CT_${i}`); const viewport = renderingEngine.getViewport(`CT_${i}`);
if (!viewport) continue; if (!viewport) continue;
const stackIdx = state.stackIdx?.[i]; const stackIdx = state.stackIdx?.[i];
@@ -4026,7 +4071,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const importedImageIds = stackObj?.i || []; const importedImageIds = stackObj?.i || [];
if (!importedImageIds.length) continue; if (!importedImageIds.length) continue;
console.log("mode", state.modes?.[i]); appLogger.debug("mode", state.modes?.[i]);
if (state.modes?.[i] === "volume") { if (state.modes?.[i] === "volume") {
try { try {
@@ -4036,7 +4081,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
viewport.setVolumes([{ volumeId }]); viewport.setVolumes([{ volumeId }]);
viewport.render(); viewport.render();
} catch (e) { } catch (e) {
console.warn("Failed to create/load volume for viewport", i, e); appLogger.warn("Failed to create/load volume for viewport", i, e);
} }
// Best-effort orientation restore // Best-effort orientation restore
if (state.orientations && state.orientations[i] && typeof viewport.setOrientation === "function") { if (state.orientations && state.orientations[i] && typeof viewport.setOrientation === "function") {
@@ -4046,7 +4091,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
setTimeout(() => { try { csUtilities.jumpToSlice(viewport.element, { imageIndex: state.volumeSliceIndices[i] }); } catch (e) { } }, 100); setTimeout(() => { try { csUtilities.jumpToSlice(viewport.element, { imageIndex: state.volumeSliceIndices[i] }); } catch (e) { } }, 100);
} }
} else { } else {
try { if (viewport.setStack) { viewport.setStack(importedImageIds); viewport.render(); } } catch (e) { console.warn('Failed to set stack for viewport', i, e); } try { if (viewport.setStack) { viewport.setStack(importedImageIds); viewport.render(); } } catch (e) { appLogger.warn('Failed to set stack for viewport', i, e); }
if (state.stackPos && typeof state.stackPos[i] === "number" && typeof viewport.setImageIdIndex === "function") { if (state.stackPos && typeof state.stackPos[i] === "number" && typeof viewport.setImageIdIndex === "function") {
try { if (csUtilities && csUtilities.jumpToSlice) csUtilities.jumpToSlice(viewport.element, { imageIndex: state.stackPos[i] }); else viewport.setImageIdIndex(state.stackPos[i]); } catch (e) { /* ignore */ } try { if (csUtilities && csUtilities.jumpToSlice) csUtilities.jumpToSlice(viewport.element, { imageIndex: state.stackPos[i] }); else viewport.setImageIdIndex(state.stackPos[i]); } catch (e) { /* ignore */ }
} }
@@ -4055,12 +4100,12 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
// Wait briefly to allow viewports to initialize GPU/actor resources // Wait briefly to allow viewports to initialize GPU/actor resources
const readyDelayMs = 1500; const readyDelayMs = 1500;
console.log(`Waiting ${readyDelayMs}ms before applying properties/camera to viewports`); appLogger.debug(`Waiting ${readyDelayMs}ms before applying properties/camera to viewports`);
await new Promise((res) => setTimeout(res, readyDelayMs)); await new Promise((res) => setTimeout(res, readyDelayMs));
// Second pass: apply renderer-sensitive properties (colormap, VOI) and camera // Second pass: apply renderer-sensitive properties (colormap, VOI) and camera
for (let i = 0; i < numActive; i++) { for (let i = 0; i < numActive; i++) {
console.log("Applying props/camera for viewport", i); appLogger.debug("Applying props/camera for viewport", i);
const viewport = renderingEngine.getViewport(`CT_${i}`); const viewport = renderingEngine.getViewport(`CT_${i}`);
if (!viewport) continue; if (!viewport) continue;
const stackIdx = state.stackIdx?.[i]; const stackIdx = state.stackIdx?.[i];
@@ -4070,7 +4115,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
// Restore properties with sanitization + retry // Restore properties with sanitization + retry
if (state.props && state.props[i] && typeof viewport.setProperties === "function") { if (state.props && state.props[i] && typeof viewport.setProperties === "function") {
console.log("Restoring properties for viewport", i, state.props[i]); appLogger.debug("Restoring properties for viewport", i, state.props[i]);
const propsToRestore = { ...state.props[i] }; const propsToRestore = { ...state.props[i] };
if (propsToRestore.colormap !== undefined && propsToRestore.colormap !== null) { if (propsToRestore.colormap !== undefined && propsToRestore.colormap !== null) {
const cm = propsToRestore.colormap; const cm = propsToRestore.colormap;
@@ -4089,8 +4134,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
viewport.setProperties(propsToRestore); viewport.setProperties(propsToRestore);
viewport.render(); viewport.render();
} catch (err) { } catch (err) {
console.warn('viewport.setProperties failed, retrying without colormap', err); appLogger.warn('viewport.setProperties failed, retrying without colormap', err);
try { const noColormap = { ...propsToRestore }; delete noColormap.colormap; viewport.setProperties(noColormap); viewport.render(); } catch (err2) { console.error('Retrying viewport.setProperties without colormap also failed', err2); } try { const noColormap = { ...propsToRestore }; delete noColormap.colormap; viewport.setProperties(noColormap); viewport.render(); } catch (err2) { appLogger.error('Retrying viewport.setProperties without colormap also failed', err2); }
} }
} }
@@ -4114,7 +4159,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
window[`exportAnnotations_${apiKey}`] = () => { window[`exportAnnotations_${apiKey}`] = () => {
try { try {
let allAnnotations = cornerstoneTools.annotation.state.getAllAnnotations(); let allAnnotations = cornerstoneTools.annotation.state.getAllAnnotations();
console.log("Exporting annotations:", allAnnotations); appLogger.debug("Exporting annotations:", allAnnotations);
// Remove ReferenceLines annotations (toolName may be 'ReferenceLines') // Remove ReferenceLines annotations (toolName may be 'ReferenceLines')
if (Array.isArray(allAnnotations)) { if (Array.isArray(allAnnotations)) {
// Only keep annotations with toolName in the allowed list // Only keep annotations with toolName in the allowed list
@@ -4158,13 +4203,13 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
if (!annotationData) continue; if (!annotationData) continue;
if (Array.isArray(annotationData)) { if (Array.isArray(annotationData)) {
annotationData.forEach(ann => { annotationData.forEach(ann => {
try { annotationState.addAnnotation(ann, ann.annotationUID); } catch (err) { console.error('Failed to add queued annotation', err, ann); } try { annotationState.addAnnotation(ann, ann.annotationUID); } catch (err) { appLogger.error('Failed to add queued annotation', err, ann); }
}); });
} }
} }
if (renderingEngineRef.current) renderingEngineRef.current.render(); if (renderingEngineRef.current) renderingEngineRef.current.render();
} catch (err) { } catch (err) {
console.error('Error flushing pending annotations:', err); appLogger.error('Error flushing pending annotations:', err);
} }
}; };
@@ -4176,7 +4221,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
// Queue the annotations until the annotation state is ready // Queue the annotations until the annotation state is ready
(window as any).__pendingImportAnnotations[apiKey] = (window as any).__pendingImportAnnotations[apiKey] || []; (window as any).__pendingImportAnnotations[apiKey] = (window as any).__pendingImportAnnotations[apiKey] || [];
(window as any).__pendingImportAnnotations[apiKey].push(annotationData); (window as any).__pendingImportAnnotations[apiKey].push(annotationData);
console.warn('importAnnotations: annotation state not ready, queued import', apiKey); appLogger.warn('importAnnotations: annotation state not ready, queued import', apiKey);
// schedule a flush attempt // schedule a flush attempt
setTimeout(() => flushPending(apiKey), 500); setTimeout(() => flushPending(apiKey), 500);
return; return;
@@ -4184,19 +4229,19 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
// Remove all existing annotations if needed // Remove all existing annotations if needed
if (annotationState.removeAllAnnotations) { if (annotationState.removeAllAnnotations) {
try { annotationState.removeAllAnnotations(); } catch (err) { console.warn('removeAllAnnotations failed', err); } try { annotationState.removeAllAnnotations(); } catch (err) { appLogger.warn('removeAllAnnotations failed', err); }
} }
if (Array.isArray(annotationData)) { if (Array.isArray(annotationData)) {
annotationData.forEach(ann => { annotationData.forEach(ann => {
try { annotationState.addAnnotation(ann, ann.annotationUID); } catch (err) { console.error('Failed to add annotation', err, ann); } try { annotationState.addAnnotation(ann, ann.annotationUID); } catch (err) { appLogger.error('Failed to add annotation', err, ann); }
}); });
} }
if (renderingEngineRef.current) { if (renderingEngineRef.current) {
renderingEngineRef.current.render(); renderingEngineRef.current.render();
} }
} catch (e) { } catch (e) {
console.error("Failed to import annotations:", e, { apiKey, cornerstoneTools: !!cornerstoneTools, renderingEngine: !!renderingEngineRef.current }); appLogger.error("Failed to import annotations:", e, { apiKey, cornerstoneTools: !!cornerstoneTools, renderingEngine: !!renderingEngineRef.current });
try { alert("Failed to import annotations: " + e); } catch (ignored) {} try { alert("Failed to import annotations: " + e); } catch (ignored) {}
} }
// Attempt to flush any queued imports (in case multiple imports were queued) // Attempt to flush any queued imports (in case multiple imports were queued)
@@ -4223,7 +4268,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
}; };
Object.entries(legacyData).forEach(([imageId, tools]) => { Object.entries(legacyData).forEach(([imageId, tools]) => {
console.log("Importing legacy annotations for imageId:", imageId); appLogger.debug("Importing legacy annotations for imageId:", imageId);
Object.entries(tools as any).forEach(([legacyTool, toolData]: [string, any]) => { Object.entries(tools as any).forEach(([legacyTool, toolData]: [string, any]) => {
const cs3Tool = toolNameMap[legacyTool] || legacyTool; const cs3Tool = toolNameMap[legacyTool] || legacyTool;
if (toolData.data && Array.isArray(toolData.data)) { if (toolData.data && Array.isArray(toolData.data)) {
@@ -4232,14 +4277,14 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const startWorld = pixelToWorld(imageId, { x: ann.handles.start.y, y: ann.handles.start.x }); const startWorld = pixelToWorld(imageId, { x: ann.handles.start.y, y: ann.handles.start.x });
const endWorld = pixelToWorld(imageId, { x: ann.handles.end.y, y: ann.handles.end.x }); const endWorld = pixelToWorld(imageId, { x: ann.handles.end.y, y: ann.handles.end.x });
console.log("Importing legacy annotation:") appLogger.debug("Importing legacy annotation:")
console.log(" Image ID:", imageId); appLogger.debug(" Image ID:", imageId);
console.log(" Start World:", startWorld); appLogger.debug(" Start World:", startWorld);
console.log(" End World:", endWorld); appLogger.debug(" End World:", endWorld);
// Get spatial metadata // Get spatial metadata
const plane = metaData.get("imagePlaneModule", imageId); const plane = metaData.get("imagePlaneModule", imageId);
console.log(metaData); appLogger.debug(metaData);
let FrameOfReferenceUID = metaData.get("frameOfReferenceUID", imageId); let FrameOfReferenceUID = metaData.get("frameOfReferenceUID", imageId);
if (!FrameOfReferenceUID && plane && plane.frameOfReferenceUID) { if (!FrameOfReferenceUID && plane && plane.frameOfReferenceUID) {
FrameOfReferenceUID = plane.frameOfReferenceUID; FrameOfReferenceUID = plane.frameOfReferenceUID;
@@ -4344,7 +4389,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
if (legacy.scale) { if (legacy.scale) {
camera.parallelScale = 200 / legacy.scale; camera.parallelScale = 200 / legacy.scale;
} }
console.log("Applying legacy camera properties:", camera); appLogger.debug("Applying legacy camera properties:", camera);
if (typeof viewport.setCamera === "function") { if (typeof viewport.setCamera === "function") {
viewport.setCamera(camera); viewport.setCamera(camera);
} }
@@ -4398,14 +4443,14 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const el = appRootRef.current; const el = appRootRef.current;
if (!el) return; if (!el) return;
function handleArrowKeyNavigation(e: KeyboardEvent) { function handleArrowKeyNavigation(e: KeyboardEvent) {
console.log("Handling arrow key navigation", e.key); appLogger.debug("Handling arrow key navigation", e.key);
console.log("activeViewport", activeViewport); appLogger.debug("activeViewport", activeViewport);
if (activeViewport === null) return; if (activeViewport === null) return;
const renderingEngine = renderingEngineRef.current; const renderingEngine = renderingEngineRef.current;
if (!renderingEngine) return; if (!renderingEngine) return;
const viewport = renderingEngine.getViewport(`CT_${activeViewport}`); const viewport = renderingEngine.getViewport(`CT_${activeViewport}`);
if (!viewport) return; if (!viewport) return;
console.log(viewport) appLogger.debug(viewport)
// --- Stack mode navigation --- // --- Stack mode navigation ---
if (viewportModes[activeViewport] === "stack") { if (viewportModes[activeViewport] === "stack") {
@@ -4536,10 +4581,10 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
const rawIds: string[] = JSON.parse(externalImageIdsStr); const rawIds: string[] = JSON.parse(externalImageIdsStr);
const zone = getDropZoneFromPointer(e); const zone = getDropZoneFromPointer(e);
applyExternalDrop(i, rawIds, stackName, zone).catch(err => { applyExternalDrop(i, rawIds, stackName, zone).catch(err => {
console.error('Failed to apply external drop:', err); appLogger.error('Failed to apply external drop:', err);
}); });
} catch (err) { } catch (err) {
console.error('Invalid text/dv3d-imageids payload:', err); appLogger.error('Invalid text/dv3d-imageids payload:', err);
} }
setDropPreview(null); setDropPreview(null);
setExternalDragActive(false); setExternalDragActive(false);
@@ -4550,7 +4595,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
if (!isNaN(stackIdx)) { if (!isNaN(stackIdx)) {
const zone = getDropZoneFromPointer(e); const zone = getDropZoneFromPointer(e);
applyStackDrop(i, stackIdx, zone).catch((err) => { applyStackDrop(i, stackIdx, zone).catch((err) => {
console.error("Failed to apply dropped stack:", err); appLogger.error("Failed to apply dropped stack:", err);
}); });
} }
setDropPreview(null); setDropPreview(null);
@@ -5664,10 +5709,10 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (invalid) { if (invalid) {
console.warn(`Attempted to select invalid stack at index ${idx}`, stack); appLogger.warn(`Attempted to select invalid stack at index ${idx}`, stack);
return; return;
} }
handleLoadStack(i, idx).catch(err => console.error("Failed to load stack:", err)); handleLoadStack(i, idx).catch(err => appLogger.error("Failed to load stack:", err));
setOpenDropdownIdx(null); setOpenDropdownIdx(null);
}} }}
> >
@@ -6390,7 +6435,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
alt={label} alt={label}
style={{ width: 56, height: 56, objectFit: 'cover', borderRadius: 4, background: '#111' }} style={{ width: 56, height: 56, objectFit: 'cover', borderRadius: 4, background: '#111' }}
onError={() => { onError={() => {
console.warn('Thumbnail <img> failed to load for stack', idx, src); appLogger.warn('Thumbnail <img> failed to load for stack', idx, src);
const usedGeneratedImage = !!generatedValid; const usedGeneratedImage = !!generatedValid;
if (usedGeneratedImage) { if (usedGeneratedImage) {
@@ -6469,6 +6514,38 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
</div> </div>
{/* Settings content: Tool dropdowns */} {/* Settings content: Tool dropdowns */}
<div style={{ marginBottom: "24px" }}> <div style={{ marginBottom: "24px" }}>
<div style={{ marginBottom: 18 }}>
<div style={{ marginBottom: 12, fontWeight: "bold" }}>Logging</div>
<div style={{ display: "flex", alignItems: "center", gap: 16, marginBottom: 10 }}>
<label style={{ display: "flex", alignItems: "center", gap: 8, cursor: "pointer" }}>
<input
type="checkbox"
checked={loggingDebugEnabled}
onChange={(e) => setLoggingDebugEnabled(e.target.checked)}
/>
Enable debug logs
</label>
</div>
<div>
<label style={{ marginRight: 8 }}>Format:</label>
<select
value={loggingFormat}
onChange={(e) => setLoggingFormat(parseLogFormat(e.target.value))}
style={{
background: "#333",
color: "#fff",
border: "1px solid #444",
borderRadius: "4px",
padding: "4px 8px",
fontSize: "14px",
}}
>
{LOG_FORMAT_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
</div>
</div>
<div style={{ marginBottom: 12, fontWeight: "bold" }}>Mouse Button Tool Bindings</div> <div style={{ marginBottom: 12, fontWeight: "bold" }}>Mouse Button Tool Bindings</div>
{MOUSE_BUTTONS.map((btn) => ( {MOUSE_BUTTONS.map((btn) => (
<div key={btn.value} style={{ marginBottom: 10 }}> <div key={btn.value} style={{ marginBottom: 10 }}>
@@ -6692,7 +6769,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
export default App export default App
function setupTools(MAIN_TOOL_GROUP_ID) { function setupTools(MAIN_TOOL_GROUP_ID) {
console.log('Setting up tools...', MAIN_TOOL_GROUP_ID); appLogger.debug('Setting up tools...', MAIN_TOOL_GROUP_ID);
cornerstoneTools.addTool(PanTool) cornerstoneTools.addTool(PanTool)
cornerstoneTools.addTool(WindowLevelTool) cornerstoneTools.addTool(WindowLevelTool)
cornerstoneTools.addTool(StackScrollTool) cornerstoneTools.addTool(StackScrollTool)
+166
View File
@@ -0,0 +1,166 @@
export type LogLevel = "debug" | "info" | "warn" | "error";
export type LogFormat = "compact" | "verbose" | "json";
export type LoggerConfig = {
debug: boolean;
format: LogFormat;
namespace: string;
dedupeWindowMs: number;
};
const DEFAULT_CONFIG: LoggerConfig = {
debug: false,
format: "compact",
namespace: "dv3d",
dedupeWindowMs: 1200,
};
let loggerConfig: LoggerConfig = { ...DEFAULT_CONFIG };
const lastMessageByFingerprint = new Map<string, number>();
type LogContext = {
namespace?: string;
dedupeKey?: string;
};
function nowIso() {
return new Date().toISOString();
}
function getConsoleMethod(level: LogLevel) {
if (level === "debug") return console.debug.bind(console);
if (level === "info") return console.info.bind(console);
if (level === "warn") return console.warn.bind(console);
return console.error.bind(console);
}
function serializeArg(arg: unknown): unknown {
if (arg instanceof Error) {
return {
name: arg.name,
message: arg.message,
stack: arg.stack,
};
}
return arg;
}
function shouldLog(level: LogLevel) {
if (level === "debug") {
return loggerConfig.debug;
}
return true;
}
function shouldDedupe(level: LogLevel) {
return level === "debug";
}
function buildFingerprint(level: LogLevel, args: unknown[], ctx?: LogContext): string {
const base = ctx?.dedupeKey || `${ctx?.namespace || loggerConfig.namespace}:${level}`;
const message = args
.map((arg) => {
if (typeof arg === "string") return arg;
try {
return JSON.stringify(serializeArg(arg));
} catch {
return String(arg);
}
})
.join("|");
return `${base}:${message}`;
}
function isDuplicate(level: LogLevel, args: unknown[], ctx?: LogContext): boolean {
if (!shouldDedupe(level) || loggerConfig.dedupeWindowMs <= 0) {
return false;
}
const fingerprint = buildFingerprint(level, args, ctx);
const t = Date.now();
const last = lastMessageByFingerprint.get(fingerprint);
lastMessageByFingerprint.set(fingerprint, t);
if (last === undefined) {
return false;
}
return t - last < loggerConfig.dedupeWindowMs;
}
function createPrefix(level: LogLevel, namespace?: string) {
const ns = namespace || loggerConfig.namespace;
if (loggerConfig.format === "compact") {
return `[${ns}] ${level.toUpperCase()}`;
}
if (loggerConfig.format === "verbose") {
return `[${nowIso()}] [${ns}] [${level.toUpperCase()}]`;
}
return "";
}
function log(level: LogLevel, args: unknown[], ctx?: LogContext) {
if (!shouldLog(level)) {
return;
}
if (isDuplicate(level, args, ctx)) {
return;
}
if (loggerConfig.format === "json") {
getConsoleMethod(level)({
ts: nowIso(),
namespace: ctx?.namespace || loggerConfig.namespace,
level,
message: args.map(serializeArg),
});
return;
}
const prefix = createPrefix(level, ctx?.namespace);
getConsoleMethod(level)(prefix, ...args);
}
export function setLoggerConfig(config: Partial<LoggerConfig>) {
loggerConfig = {
...loggerConfig,
...config,
};
return getLoggerConfig();
}
export function getLoggerConfig(): LoggerConfig {
return { ...loggerConfig };
}
export function parseLogFormat(value: unknown): LogFormat {
if (value === "compact" || value === "verbose" || value === "json") {
return value;
}
return DEFAULT_CONFIG.format;
}
export function parseDebugFlag(value: unknown): boolean {
if (typeof value === "boolean") {
return value;
}
if (typeof value === "string") {
const normalized = value.trim().toLowerCase();
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
}
if (typeof value === "number") {
return value !== 0;
}
return false;
}
export const logger = {
debug: (...args: unknown[]) => log("debug", args),
info: (...args: unknown[]) => log("info", args),
warn: (...args: unknown[]) => log("warn", args),
error: (...args: unknown[]) => log("error", args),
child: (namespace: string) => ({
debug: (...args: unknown[]) => log("debug", args, { namespace }),
info: (...args: unknown[]) => log("info", args, { namespace }),
warn: (...args: unknown[]) => log("warn", args, { namespace }),
error: (...args: unknown[]) => log("error", args, { namespace }),
}),
};
+69 -37
View File
@@ -1,12 +1,24 @@
import React from 'react' import React from "react";
import { createRoot, type Root } from "react-dom/client"; import { createRoot, type Root } from "react-dom/client";
import App from './App.tsx' import App from "./App.tsx";
import './index.css' import "./index.css";
import { availableImageIds } from "./lib/createImageIds.ts" import { availableImageIds } from "./lib/createImageIds.ts";
import {
getLoggerConfig,
logger,
parseDebugFlag,
parseLogFormat,
setLoggerConfig,
type LogFormat,
} from "./lib/logger";
type MountOptions = { type MountOptions = {
force?: boolean; force?: boolean;
containerIds?: string[]; containerIds?: string[];
logging?: {
debug?: boolean;
format?: LogFormat;
};
}; };
type ViewerDescriptor = { type ViewerDescriptor = {
@@ -22,11 +34,14 @@ type RootRecord = {
}; };
const rootRegistry = new WeakMap<HTMLElement, RootRecord>(); const rootRegistry = new WeakMap<HTMLElement, RootRecord>();
const mainLogger = logger.child("main");
declare global { declare global {
interface Window { interface Window {
mountDicomViewers?: (options?: MountOptions) => void; mountDicomViewers?: (options?: MountOptions) => void;
remountDicomViewer?: (containerId: string) => void; remountDicomViewer?: (containerId: string) => void;
setDicomViewerLogging?: (config: { debug?: boolean; format?: LogFormat }) => void;
getDicomViewerLogging?: () => { debug: boolean; format: LogFormat; namespace: string; dedupeWindowMs: number };
} }
} }
@@ -83,8 +98,8 @@ const parseToDescriptors = (parsed: any): Array<ViewerDescriptor> => {
else if (Array.isArray(s.imageIds)) imageIds = s.imageIds; else if (Array.isArray(s.imageIds)) imageIds = s.imageIds;
else if (Array.isArray(s.i)) imageIds = s.i; else if (Array.isArray(s.i)) imageIds = s.i;
if (!Array.isArray(imageIds) || imageIds.length === 0 || !imageIds.every(x => typeof x === "string")) { if (!Array.isArray(imageIds) || imageIds.length === 0 || !imageIds.every((x) => typeof x === "string")) {
console.warn(`data-named-stacks: skipping invalid nested stack at parsed[${itemIdx}].stacks[${sIdx}]`, s); mainLogger.warn(`data-named-stacks: skipping invalid nested stack at parsed[${itemIdx}].stacks[${sIdx}]`, s);
return; return;
} }
@@ -98,10 +113,10 @@ const parseToDescriptors = (parsed: any): Array<ViewerDescriptor> => {
return; return;
} }
if (item && typeof item === 'object' && (Array.isArray(item.imageIds) || Array.isArray(item.i))) { if (item && typeof item === "object" && (Array.isArray(item.imageIds) || Array.isArray(item.i))) {
const imageIds = Array.isArray(item.imageIds) ? item.imageIds : item.i; const imageIds = Array.isArray(item.imageIds) ? item.imageIds : item.i;
if (!Array.isArray(imageIds) || imageIds.length === 0 || !imageIds.every(x => typeof x === "string")) { if (!Array.isArray(imageIds) || imageIds.length === 0 || !imageIds.every((x) => typeof x === "string")) {
console.warn(`data-named-stacks: skipping invalid stack object at parsed[${itemIdx}]`, item); mainLogger.warn(`data-named-stacks: skipping invalid stack object at parsed[${itemIdx}]`, item);
return; return;
} }
@@ -119,7 +134,7 @@ const parseToDescriptors = (parsed: any): Array<ViewerDescriptor> => {
return; return;
} }
console.warn(`data-named-stacks: unknown or invalid entry at parsed[${itemIdx}] skipping`, item); mainLogger.warn(`data-named-stacks: unknown or invalid entry at parsed[${itemIdx}] - skipping`, item);
}); });
return out; return out;
@@ -127,24 +142,35 @@ const parseToDescriptors = (parsed: any): Array<ViewerDescriptor> => {
const getMountSignature = (container: HTMLElement): string => { const getMountSignature = (container: HTMLElement): string => {
return JSON.stringify({ return JSON.stringify({
id: container.id || '', id: container.id || "",
empty: container.getAttribute('data-empty') || '', empty: container.getAttribute("data-empty") || "",
namedStacks: container.getAttribute('data-named-stacks') || '', namedStacks: container.getAttribute("data-named-stacks") || "",
images: container.getAttribute('data-images') || '', images: container.getAttribute("data-images") || "",
autoCache: container.getAttribute('data-auto-cache-stack') || '', autoCache: container.getAttribute("data-auto-cache-stack") || "",
annotation: container.getAttribute('data-annotationjson') || '', annotation: container.getAttribute("data-annotationjson") || "",
viewerState: container.getAttribute('data-viewerstate') || '', viewerState: container.getAttribute("data-viewerstate") || "",
}); });
}; };
const shouldMountContainer = (container: HTMLElement, options?: MountOptions): boolean => { const shouldMountContainer = (container: HTMLElement, options?: MountOptions): boolean => {
if (!options?.containerIds || options.containerIds.length === 0) return true; if (!options?.containerIds || options.containerIds.length === 0) return true;
return options.containerIds.includes(container.id || ''); return options.containerIds.includes(container.id || "");
};
const getContainerLogConfig = (container: HTMLElement, options?: MountOptions) => {
const attrDebug = container.getAttribute("data-log-debug");
const attrFormat = container.getAttribute("data-log-format");
const debug = attrDebug !== null ? parseDebugFlag(attrDebug) : !!options?.logging?.debug;
const format = parseLogFormat(attrFormat ?? options?.logging?.format);
return { debug, format };
}; };
const mountContainer = (container: HTMLElement, imageStacks: () => Promise<any[]>, options?: MountOptions) => { const mountContainer = (container: HTMLElement, imageStacks: () => Promise<any[]>, options?: MountOptions) => {
const signature = getMountSignature(container); const signature = getMountSignature(container);
const existing = rootRegistry.get(container); const existing = rootRegistry.get(container);
const initialLoggerConfig = getContainerLogConfig(container, options);
if (existing && !options?.force && existing.signature === signature) { if (existing && !options?.force && existing.signature === signature) {
return; return;
@@ -155,14 +181,17 @@ const mountContainer = (container: HTMLElement, imageStacks: () => Promise<any[]
rootRegistry.delete(container); rootRegistry.delete(container);
} }
setLoggerConfig(initialLoggerConfig);
const root = createRoot(container); const root = createRoot(container);
root.render( root.render(
<App <App
container_id={container.id || ''} container_id={container.id || ""}
imageStacks={imageStacks} imageStacks={imageStacks}
autoCacheStack={(container.getAttribute('data-auto-cache-stack') === "true" || container.getAttribute('data-auto-cache-stack') === "1")} autoCacheStack={container.getAttribute("data-auto-cache-stack") === "true" || container.getAttribute("data-auto-cache-stack") === "1"}
annotationJson={container.getAttribute('data-annotationjson') || undefined} annotationJson={container.getAttribute("data-annotationjson") || undefined}
viewerState={container.getAttribute('data-viewerstate') || undefined} viewerState={container.getAttribute("data-viewerstate") || undefined}
initialLoggerConfig={initialLoggerConfig}
/> />
); );
@@ -171,10 +200,9 @@ const mountContainer = (container: HTMLElement, imageStacks: () => Promise<any[]
export function mountDicomViewers(options?: MountOptions) { export function mountDicomViewers(options?: MountOptions) {
// Test containers // Test containers
const test_containers = document.querySelectorAll(".dicom-viewer-test-root"); const testContainers = document.querySelectorAll(".dicom-viewer-test-root");
test_containers.forEach((container) => { testContainers.forEach((container) => {
if (!shouldMountContainer(container as HTMLElement, options)) return; if (!shouldMountContainer(container as HTMLElement, options)) return;
const id = container.id || '';
const imageStacks = () => availableImageIds(); const imageStacks = () => availableImageIds();
mountContainer(container as HTMLElement, imageStacks, options); mountContainer(container as HTMLElement, imageStacks, options);
}); });
@@ -185,10 +213,10 @@ export function mountDicomViewers(options?: MountOptions) {
if (!shouldMountContainer(container as HTMLElement, options)) return; if (!shouldMountContainer(container as HTMLElement, options)) return;
let imageStacks: () => Promise<any[]>; let imageStacks: () => Promise<any[]>;
const isEmpty = container.getAttribute('data-empty') === 'true'; const isEmpty = container.getAttribute("data-empty") === "true";
const dataNamed = container.getAttribute('data-named-stacks'); const dataNamed = container.getAttribute("data-named-stacks");
const dataImages = container.getAttribute('data-images'); const dataImages = container.getAttribute("data-images");
if (isEmpty) { if (isEmpty) {
imageStacks = () => Promise.resolve([] as any[]); imageStacks = () => Promise.resolve([] as any[]);
@@ -197,7 +225,7 @@ export function mountDicomViewers(options?: MountOptions) {
try { try {
parsed = JSON.parse(dataNamed); parsed = JSON.parse(dataNamed);
} catch (e) { } catch (e) {
console.error("Invalid data-named-stacks JSON:", e); mainLogger.error("Invalid data-named-stacks JSON:", e);
parsed = []; parsed = [];
} }
const descriptors = parseToDescriptors(parsed); const descriptors = parseToDescriptors(parsed);
@@ -207,15 +235,14 @@ export function mountDicomViewers(options?: MountOptions) {
try { try {
parsed = JSON.parse(dataImages); parsed = JSON.parse(dataImages);
} catch (e) { } catch (e) {
console.error("Invalid data-images JSON:", e); mainLogger.error("Invalid data-images JSON:", e);
parsed = []; parsed = [];
} }
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)) {
imageStacks = () => Promise.resolve(parsed.map( imageStacks = () =>
stack => Array.isArray(stack) ? toWadouri(stack) : [] Promise.resolve(parsed.map((stack) => (Array.isArray(stack) ? toWadouri(stack) : [])));
));
} else { } else {
imageStacks = () => Promise.resolve([[]]); imageStacks = () => Promise.resolve([[]]);
} }
@@ -227,11 +254,16 @@ export function mountDicomViewers(options?: MountOptions) {
}); });
} }
// Optionally, call it immediately for static containers:
mountDicomViewers(); mountDicomViewers();
// Expose globally for dynamic use. window.mountDicomViewers = mountDicomViewers;
(window as any).mountDicomViewers = mountDicomViewers; window.remountDicomViewer = (containerId: string) => {
(window as any).remountDicomViewer = (containerId: string) => {
mountDicomViewers({ force: true, containerIds: [containerId] }); mountDicomViewers({ force: true, containerIds: [containerId] });
}; };
window.setDicomViewerLogging = (config) => {
setLoggerConfig({
debug: config.debug,
format: config.format ? parseLogFormat(config.format) : undefined,
});
};
window.getDicomViewerLogging = () => getLoggerConfig();
+4 -1
View File
@@ -1,5 +1,8 @@
import React from 'react' import React from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { logger } from './lib/logger'
const testLogger = logger.child('nonDicomTest')
// Sample external images (use CORS-friendly URLs). These are Wikimedia Commons images. // Sample external images (use CORS-friendly URLs). These are Wikimedia Commons images.
const sampleImages = [ const sampleImages = [
@@ -38,7 +41,7 @@ function mount() {
</React.StrictMode> </React.StrictMode>
) )
}).catch(err => { }).catch(err => {
console.error('Failed to load App for non-dicom test', err) testLogger.error('Failed to load App for non-dicom test', err)
}) })
} }