From 4dd63668b0e3307646a15f1e1e6d1a81242cadfc Mon Sep 17 00:00:00 2001 From: Ross Date: Tue, 19 May 2026 09:41:52 +0100 Subject: [PATCH] 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. --- dicom-viewer/src/App.tsx | 255 +++++++++++++++++++----------- dicom-viewer/src/lib/logger.ts | 166 +++++++++++++++++++ dicom-viewer/src/main.tsx | 108 ++++++++----- dicom-viewer/src/nonDicomTest.tsx | 5 +- 4 files changed, 406 insertions(+), 128 deletions(-) create mode 100644 dicom-viewer/src/lib/logger.ts diff --git a/dicom-viewer/src/App.tsx b/dicom-viewer/src/App.tsx index 3fedfbf..a4ff263 100644 --- a/dicom-viewer/src/App.tsx +++ b/dicom-viewer/src/App.tsx @@ -18,6 +18,7 @@ import { volumeLoader } from '@cornerstonejs/core'; import { metaData } from '@cornerstonejs/core'; import * as dicomParser from "dicom-parser"; import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader" +import { getLoggerConfig, logger, parseLogFormat, setLoggerConfig, type LogFormat } from "./lib/logger"; import { NoLabelArrowAnnotateTool } from "./NoLabelArrowAnnotateTool"; 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_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) { if (!toolName) return '🔖'; @@ -301,11 +309,15 @@ type AppProps = { autoCacheStack?: boolean; // <-- new prop annotationJson?: string; viewerState?: string; + initialLoggerConfig?: { + debug?: boolean; + format?: LogFormat; + }; // ...other props if needed }; // 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(null); const elementRef = useRef(null) const running = useRef(false) @@ -313,6 +325,10 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer const MOUSE_SETTINGS_KEY = "dv3d_mouseToolBindings"; 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(initialLogFormat); const [settingsOpen, setSettingsOpen] = useState(false); // Settings open state @@ -369,11 +385,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer // If called with a function, log the result _setViewportImageIds(prev => { const next = (value as (prev: string[][]) => string[][])(prev); - console.log("setViewportImageIds (fn):", next); + appLogger.debug("setViewportImageIds (fn):", next); return next; }); } else { - console.log("setViewportImageIds:", value); + appLogger.debug("setViewportImageIds:", value); _setViewportImageIds(value); } }; @@ -686,7 +702,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer msg.toLowerCase().includes('webgl') || msg.toLowerCase().includes('colormap') ) { - console.error('Detected graphics/rendering error:', ev); + appLogger.error('Detected graphics/rendering error:', ev); setGraphicsErrorMessage(msg || null); setGraphicsErrorVisible(true); } @@ -708,7 +724,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer text.toLowerCase().includes('webgl') || text.toLowerCase().includes('colormap') ) { - console.error('Detected graphics/rendering rejection:', ev); + appLogger.error('Detected graphics/rendering rejection:', ev); setGraphicsErrorMessage(text || null); setGraphicsErrorVisible(true); } @@ -741,7 +757,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer fn(annotationJson); } } catch (e) { - console.error("Failed to import annotations from prop:", e); + appLogger.error("Failed to import annotations from prop:", e); } } if (viewerState) { @@ -751,7 +767,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer fn(viewerState); } } catch (e) { - console.error("Failed to import viewer state from prop:", e); + appLogger.error("Failed to import viewer state from prop:", e); } } setLoadingState(null); @@ -865,7 +881,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer } }) .catch(err => { - console.error("Failed to load imageStacks:", err); + appLogger.error("Failed to load imageStacks:", err); setAvailableStacks([]); setViewportImageIds(Array(MAX_VIEWPORTS).fill([])); }); @@ -877,16 +893,16 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer const stackObj = availableStacks[stackIdx]; if (!stackObj) return; 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 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; await setLoadedImageIdsAndCacheMeta(stack, { force: true }); 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 => { const next = [...prev]; if (!next[stackIdx]) return prev; @@ -895,7 +911,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer }); 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"); try { @@ -941,7 +957,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer const [showAdvancedCtrlMouseBindings, setShowAdvancedCtrlMouseBindings] = useState(false); const sortViewportImageIdsBySliceLocation = () => { - console.log("Sorting viewport imageIds by slice location"); + appLogger.debug("Sorting viewport imageIds by slice location"); setViewportImageIds(prev => { const next = prev.map((imageIds, idx) => { 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 const setLoadedImageIdsAndCacheMeta = async (imageIds: string[], options?: { force?: boolean }) => { - console.log("Setting loaded imageIds:", imageIds); - console.log("autoCacheStack:", autoCacheStack); + appLogger.debug("Setting loaded imageIds:", imageIds); + appLogger.debug("autoCacheStack:", autoCacheStack); const loadPromises: Promise[] = []; const shouldCache = Boolean(autoCacheStack || options?.force); @@ -1175,7 +1191,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer urlsToLoad.add(baseUrl); } } 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); } } 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 if (orderBySliceLocation) { - console.log("Sorting viewport imageIds by slice location"); + appLogger.debug("Sorting viewport imageIds by slice location"); sortViewportImageIdsBySliceLocation(); } } @@ -1213,7 +1229,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer const cacheKey = `wadouri:${url}`; const cached = multiframeCountCacheRef.current[cacheKey]; 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; } @@ -1227,7 +1243,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer const multiframeMeta = metaData.get("multiframeModule", normalized) as any; const fromMeta = Number(multiframeMeta?.numberOfFrames); 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); return Math.floor(fromMeta); } @@ -1240,7 +1256,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer if (cacheManager?.get) { const dataSet = cacheManager.get(url); const fromCache = readFramesFromDataSet(dataSet); - console.log("[getNumberOfFramesForImageId] Found in cache manager:", fromCache); + appLogger.debug("[getNumberOfFramesForImageId] Found in cache manager:", fromCache); multiframeCountCacheRef.current[cacheKey] = fromCache; return fromCache; } @@ -1249,16 +1265,16 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer } try { - console.log("[getNumberOfFramesForImageId] Fetching:", url.slice(0, 80)); + appLogger.debug("[getNumberOfFramesForImageId] Fetching:", url.slice(0, 80)); const response = await fetch(url); const arrayBuffer = await response.arrayBuffer(); const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer)); const fromFetch = readFramesFromDataSet(dataSet); - console.log("[getNumberOfFramesForImageId] Found via fetch:", fromFetch); + appLogger.debug("[getNumberOfFramesForImageId] Found via fetch:", fromFetch); multiframeCountCacheRef.current[cacheKey] = fromFetch; return fromFetch; } catch (e) { - console.log("[getNumberOfFramesForImageId] Fetch failed, returning 1:", e); + appLogger.debug("[getNumberOfFramesForImageId] Fetch failed, returning 1:", e); multiframeCountCacheRef.current[cacheKey] = 1; return 1; } @@ -1266,7 +1282,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer const expandMultiframeImageIds = useCallback(async (rawImageIds: string[]) => { const expanded: string[] = []; - console.log("[expandMultiframeImageIds] Input:", rawImageIds.length, "imageIds"); + appLogger.debug("[expandMultiframeImageIds] Input:", rawImageIds.length, "imageIds"); for (const raw of rawImageIds || []) { const imageId = normalizeToWadouriOrExternal(raw); @@ -1276,7 +1292,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer } 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) { expanded.push(imageId); continue; @@ -1285,11 +1301,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer for (let frame = 1; frame <= frameCount; frame++) { expanded.push(withFrameQuery(imageId, frame)); } - console.log("[expandMultiframeImageIds] Expanded to", frameCount, "frames"); + appLogger.debug("[expandMultiframeImageIds] Expanded to", frameCount, "frames"); } const result = dedupeImageIds(expanded); - console.log("[expandMultiframeImageIds] Output:", result.length, "total imageIds"); + appLogger.debug("[expandMultiframeImageIds] Output:", result.length, "total imageIds"); return result; }, [getNumberOfFramesForImageId]); @@ -1689,6 +1705,17 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer const ctrl = localStorage.getItem(CTRL_MOUSE_SETTINGS_KEY); if (mouse) setMouseToolBindings(JSON.parse(mouse)); 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) { // Ignore parse errors } @@ -1707,6 +1734,24 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer } catch (e) { } }, [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(() => { window.loadDicomStackFromFiles = (files: FileList | File[]) => { const fileArray = Array.from(files); @@ -1791,7 +1836,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer visibleStackImageIds.forEach((ids) => { 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) { // ignore individual entry errors - console.debug('collectFilesFromDirectoryHandle: skipping entry due to error', e); + appLogger.debug('collectFilesFromDirectoryHandle: skipping entry due to error', e); } } return files; @@ -2020,7 +2065,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer } return; } catch (e) { - console.warn('showDirectoryPicker failed or cancelled', e); + appLogger.warn('showDirectoryPicker failed or cancelled', e); // fallthrough to input click } } @@ -2081,7 +2126,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer for (const entry of parsedEntries) { // Skip files that are neither images nor successfully parsed DICOMs 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; } 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]; 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 if (currentImageId.includes('&frame=')) { const match = currentImageId.match(/&frame=(\d+)/); if (match) { frameIndex = parseInt(match[1], 10); - console.log("[updateOverlay] Frame detected:", frameIndex); + appLogger.debug("[updateOverlay] Frame detected:", frameIndex); // Try to get total frames from metadata const baseImageId = currentImageId.split('&frame=')[0]; const multiframeMeta = metaData.get("multiframeModule", baseImageId) as any; if (multiframeMeta?.numberOfFrames) { totalFrames = multiframeMeta.numberOfFrames; - console.log("[updateOverlay] Total frames from metadata:", totalFrames); + appLogger.debug("[updateOverlay] Total frames from metadata:", totalFrames); } else { // Fallback: count unique frame indices in imageIds const frameNumbers = imageIds.map(id => { @@ -2266,12 +2311,12 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer }).filter(n => n !== null && typeof n === 'number'); if (frameNumbers.length > 0) { totalFrames = Math.max(...frameNumbers as number[]); - console.log("[updateOverlay] Total frames from imageIds:", totalFrames); + appLogger.debug("[updateOverlay] Total frames from imageIds:", totalFrames); } } } } 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; @@ -2692,7 +2737,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer useEffect(() => { const setup = async () => { - console.log("setup") + appLogger.debug("setup") // 1. Initialize Cornerstone3D and tools ONCE if (!running.current) { @@ -2750,7 +2795,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer return { promise }; }); } 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 mainToolGroup.removeViewports(renderingEngineId, viewportId); } catch (err) { - console.warn('Failed to render external image in viewport', err); + appLogger.warn('Failed to render external image in viewport', err); } } else { try { viewport.setStack(imageIds); } 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; } setTimeout(() => { - console.log("Restoring properties (timeout) for viewport", i, propsToRestore); + appLogger.debug("Restoring properties (timeout) for viewport", i, propsToRestore); viewport.setProperties(propsToRestore); viewport.render() }, 100); @@ -3056,7 +3101,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer if (viewport && typeof viewport.resetCamera === "function") { 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.render(); } @@ -3070,7 +3115,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer if (renderingEngine) { const viewport = renderingEngine.getViewport(viewportId); 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({ voiRange: { lower: center - width / 2, @@ -3140,7 +3185,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer updateOverlay(viewportIdx, viewport); return true; } catch (err) { - console.warn("Failed to apply default window level", err); + appLogger.warn("Failed to apply default window level", err); return false; } }, [updateOverlay, viewportImageIds]); @@ -3708,8 +3753,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer function truncateStack(lower: number, upper: number) { // Only allow when 1 viewport and 1 stack is loaded - console.log("Truncating stack from", lower, "to", upper); - console.log(stackOrderModified) + appLogger.debug("Truncating stack from", lower, "to", upper); + appLogger.debug(stackOrderModified) if ( viewportGrid.rows !== 1 || viewportGrid.cols !== 1 || @@ -3880,8 +3925,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer u: s.studyInstanceUID, // studyInstanceUID g: s.seriesGroups, })); - console.log("Exporting viewer state with", numActive, "active viewports"); - console.log("modes", modes); + appLogger.debug("Exporting viewer state with", numActive, "active viewports"); + appLogger.debug("modes", modes); // Only keep minimal properties const renderingEngine = renderingEngineRef.current; @@ -3892,9 +3937,9 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer const volumeSliceIndices: (number | null)[] = []; for (let i = 0; i < numActive; i++) { 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) { - 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 viewport = renderingEngine.getViewport(viewportId); if (viewport) { @@ -3908,11 +3953,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer if (typeof viewport.getCamera === "function") { c = viewport.getCamera(); } - console.log("viewport", viewportId, "properties", p); + appLogger.debug("viewport", viewportId, "properties", p); // --- Volume orientation and slice index --- if (modes[i] === "volume") { - console.log("Getting orientation for viewport", i); - console.log("viewport.viewportProperties", viewport.viewportProperties); + appLogger.debug("Getting orientation for viewport", i); + appLogger.debug("viewport.viewportProperties", viewport.viewportProperties); o = viewport.viewportProperties?.orientation || null; if (typeof viewport.getSliceIndex === "function") { vIdx = viewport.getSliceIndex(); @@ -3985,7 +4030,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer const numActive = state.grid?.rows * state.grid?.cols || 1; let allReady = true; for (let i = 0; i < numActive; i++) { - console.log("Checking viewport", i); + appLogger.debug("Checking viewport", i); const viewport = renderingEngine.getViewport(`CT_${i}`); if (!viewport) { allReady = false; break; } const stackIdx = state.stackIdx?.[i]; @@ -3998,7 +4043,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer viewportImageIds.length !== importedImageIds.length || !viewportImageIds.every((id, idx) => id === importedImageIds[idx]) ) { - console.warn( + appLogger.warn( `Viewport ${i}: importedImageIds do not match 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 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}`); if (!viewport) continue; const stackIdx = state.stackIdx?.[i]; @@ -4026,7 +4071,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer const importedImageIds = stackObj?.i || []; if (!importedImageIds.length) continue; - console.log("mode", state.modes?.[i]); + appLogger.debug("mode", state.modes?.[i]); if (state.modes?.[i] === "volume") { try { @@ -4036,7 +4081,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer viewport.setVolumes([{ volumeId }]); viewport.render(); } 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 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); } } 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") { 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 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)); // Second pass: apply renderer-sensitive properties (colormap, VOI) and camera 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}`); if (!viewport) continue; const stackIdx = state.stackIdx?.[i]; @@ -4070,7 +4115,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer // Restore properties with sanitization + retry 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] }; if (propsToRestore.colormap !== undefined && propsToRestore.colormap !== null) { const cm = propsToRestore.colormap; @@ -4089,8 +4134,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer viewport.setProperties(propsToRestore); viewport.render(); } catch (err) { - console.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); } + appLogger.warn('viewport.setProperties failed, retrying without colormap', err); + 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}`] = () => { try { let allAnnotations = cornerstoneTools.annotation.state.getAllAnnotations(); - console.log("Exporting annotations:", allAnnotations); + appLogger.debug("Exporting annotations:", allAnnotations); // Remove ReferenceLines annotations (toolName may be 'ReferenceLines') if (Array.isArray(allAnnotations)) { // 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 (Array.isArray(annotationData)) { 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(); } 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 (window as any).__pendingImportAnnotations[apiKey] = (window as any).__pendingImportAnnotations[apiKey] || []; (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 setTimeout(() => flushPending(apiKey), 500); return; @@ -4184,19 +4229,19 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer // Remove all existing annotations if needed 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)) { 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) { renderingEngineRef.current.render(); } } 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) {} } // 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]) => { - 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]) => { const cs3Tool = toolNameMap[legacyTool] || legacyTool; 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 endWorld = pixelToWorld(imageId, { x: ann.handles.end.y, y: ann.handles.end.x }); - console.log("Importing legacy annotation:") - console.log(" Image ID:", imageId); - console.log(" Start World:", startWorld); - console.log(" End World:", endWorld); + appLogger.debug("Importing legacy annotation:") + appLogger.debug(" Image ID:", imageId); + appLogger.debug(" Start World:", startWorld); + appLogger.debug(" End World:", endWorld); // Get spatial metadata const plane = metaData.get("imagePlaneModule", imageId); - console.log(metaData); + appLogger.debug(metaData); let FrameOfReferenceUID = metaData.get("frameOfReferenceUID", imageId); if (!FrameOfReferenceUID && plane && plane.frameOfReferenceUID) { FrameOfReferenceUID = plane.frameOfReferenceUID; @@ -4344,7 +4389,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer if (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") { viewport.setCamera(camera); } @@ -4398,14 +4443,14 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer const el = appRootRef.current; if (!el) return; function handleArrowKeyNavigation(e: KeyboardEvent) { - console.log("Handling arrow key navigation", e.key); - console.log("activeViewport", activeViewport); + appLogger.debug("Handling arrow key navigation", e.key); + appLogger.debug("activeViewport", activeViewport); if (activeViewport === null) return; const renderingEngine = renderingEngineRef.current; if (!renderingEngine) return; const viewport = renderingEngine.getViewport(`CT_${activeViewport}`); if (!viewport) return; - console.log(viewport) + appLogger.debug(viewport) // --- Stack mode navigation --- if (viewportModes[activeViewport] === "stack") { @@ -4536,10 +4581,10 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer const rawIds: string[] = JSON.parse(externalImageIdsStr); const zone = getDropZoneFromPointer(e); 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) { - console.error('Invalid text/dv3d-imageids payload:', err); + appLogger.error('Invalid text/dv3d-imageids payload:', err); } setDropPreview(null); setExternalDragActive(false); @@ -4550,7 +4595,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer if (!isNaN(stackIdx)) { const zone = getDropZoneFromPointer(e); applyStackDrop(i, stackIdx, zone).catch((err) => { - console.error("Failed to apply dropped stack:", err); + appLogger.error("Failed to apply dropped stack:", err); }); } setDropPreview(null); @@ -5664,10 +5709,10 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer onClick={(e) => { e.stopPropagation(); 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; } - 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); }} > @@ -6390,7 +6435,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer alt={label} style={{ width: 56, height: 56, objectFit: 'cover', borderRadius: 4, background: '#111' }} onError={() => { - console.warn('Thumbnail failed to load for stack', idx, src); + appLogger.warn('Thumbnail failed to load for stack', idx, src); const usedGeneratedImage = !!generatedValid; if (usedGeneratedImage) { @@ -6469,6 +6514,38 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer {/* Settings content: Tool dropdowns */}
+
+
Logging
+
+ +
+
+ + +
+
Mouse Button Tool Bindings
{MOUSE_BUTTONS.map((btn) => (
@@ -6692,7 +6769,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer export default App 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(WindowLevelTool) cornerstoneTools.addTool(StackScrollTool) diff --git a/dicom-viewer/src/lib/logger.ts b/dicom-viewer/src/lib/logger.ts new file mode 100644 index 0000000..98ae637 --- /dev/null +++ b/dicom-viewer/src/lib/logger.ts @@ -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(); + +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, + ...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 }), + }), +}; diff --git a/dicom-viewer/src/main.tsx b/dicom-viewer/src/main.tsx index 166c78c..b827184 100644 --- a/dicom-viewer/src/main.tsx +++ b/dicom-viewer/src/main.tsx @@ -1,12 +1,24 @@ -import React from 'react' +import React from "react"; import { createRoot, type Root } from "react-dom/client"; -import App from './App.tsx' -import './index.css' -import { availableImageIds } from "./lib/createImageIds.ts" +import App from "./App.tsx"; +import "./index.css"; +import { availableImageIds } from "./lib/createImageIds.ts"; +import { + getLoggerConfig, + logger, + parseDebugFlag, + parseLogFormat, + setLoggerConfig, + type LogFormat, +} from "./lib/logger"; type MountOptions = { force?: boolean; containerIds?: string[]; + logging?: { + debug?: boolean; + format?: LogFormat; + }; }; type ViewerDescriptor = { @@ -22,11 +34,14 @@ type RootRecord = { }; const rootRegistry = new WeakMap(); +const mainLogger = logger.child("main"); declare global { interface Window { mountDicomViewers?: (options?: MountOptions) => 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 => { else if (Array.isArray(s.imageIds)) imageIds = s.imageIds; else if (Array.isArray(s.i)) imageIds = s.i; - 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); + if (!Array.isArray(imageIds) || imageIds.length === 0 || !imageIds.every((x) => typeof x === "string")) { + mainLogger.warn(`data-named-stacks: skipping invalid nested stack at parsed[${itemIdx}].stacks[${sIdx}]`, s); return; } @@ -98,10 +113,10 @@ const parseToDescriptors = (parsed: any): Array => { 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; - 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); + if (!Array.isArray(imageIds) || imageIds.length === 0 || !imageIds.every((x) => typeof x === "string")) { + mainLogger.warn(`data-named-stacks: skipping invalid stack object at parsed[${itemIdx}]`, item); return; } @@ -119,7 +134,7 @@ const parseToDescriptors = (parsed: any): Array => { 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; @@ -127,24 +142,35 @@ const parseToDescriptors = (parsed: any): Array => { const getMountSignature = (container: HTMLElement): string => { return JSON.stringify({ - id: container.id || '', - empty: container.getAttribute('data-empty') || '', - namedStacks: container.getAttribute('data-named-stacks') || '', - images: container.getAttribute('data-images') || '', - autoCache: container.getAttribute('data-auto-cache-stack') || '', - annotation: container.getAttribute('data-annotationjson') || '', - viewerState: container.getAttribute('data-viewerstate') || '', + id: container.id || "", + empty: container.getAttribute("data-empty") || "", + namedStacks: container.getAttribute("data-named-stacks") || "", + images: container.getAttribute("data-images") || "", + autoCache: container.getAttribute("data-auto-cache-stack") || "", + annotation: container.getAttribute("data-annotationjson") || "", + viewerState: container.getAttribute("data-viewerstate") || "", }); }; const shouldMountContainer = (container: HTMLElement, options?: MountOptions): boolean => { 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, options?: MountOptions) => { const signature = getMountSignature(container); const existing = rootRegistry.get(container); + const initialLoggerConfig = getContainerLogConfig(container, options); if (existing && !options?.force && existing.signature === signature) { return; @@ -155,14 +181,17 @@ const mountContainer = (container: HTMLElement, imageStacks: () => Promise ); @@ -171,10 +200,9 @@ const mountContainer = (container: HTMLElement, imageStacks: () => Promise { + const testContainers = document.querySelectorAll(".dicom-viewer-test-root"); + testContainers.forEach((container) => { if (!shouldMountContainer(container as HTMLElement, options)) return; - const id = container.id || ''; const imageStacks = () => availableImageIds(); mountContainer(container as HTMLElement, imageStacks, options); }); @@ -185,10 +213,10 @@ export function mountDicomViewers(options?: MountOptions) { if (!shouldMountContainer(container as HTMLElement, options)) return; let imageStacks: () => Promise; - const isEmpty = container.getAttribute('data-empty') === 'true'; + const isEmpty = container.getAttribute("data-empty") === "true"; - const dataNamed = container.getAttribute('data-named-stacks'); - const dataImages = container.getAttribute('data-images'); + const dataNamed = container.getAttribute("data-named-stacks"); + const dataImages = container.getAttribute("data-images"); if (isEmpty) { imageStacks = () => Promise.resolve([] as any[]); @@ -197,7 +225,7 @@ export function mountDicomViewers(options?: MountOptions) { try { parsed = JSON.parse(dataNamed); } catch (e) { - console.error("Invalid data-named-stacks JSON:", e); + mainLogger.error("Invalid data-named-stacks JSON:", e); parsed = []; } const descriptors = parseToDescriptors(parsed); @@ -207,15 +235,14 @@ export function mountDicomViewers(options?: MountOptions) { try { parsed = JSON.parse(dataImages); } catch (e) { - console.error("Invalid data-images JSON:", e); + mainLogger.error("Invalid data-images JSON:", e); parsed = []; } if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") { imageStacks = () => Promise.resolve([toWadouri(parsed)]); } else if (Array.isArray(parsed)) { - imageStacks = () => Promise.resolve(parsed.map( - stack => Array.isArray(stack) ? toWadouri(stack) : [] - )); + imageStacks = () => + Promise.resolve(parsed.map((stack) => (Array.isArray(stack) ? toWadouri(stack) : []))); } else { imageStacks = () => Promise.resolve([[]]); } @@ -227,11 +254,16 @@ export function mountDicomViewers(options?: MountOptions) { }); } -// Optionally, call it immediately for static containers: mountDicomViewers(); -// Expose globally for dynamic use. -(window as any).mountDicomViewers = mountDicomViewers; -(window as any).remountDicomViewer = (containerId: string) => { +window.mountDicomViewers = mountDicomViewers; +window.remountDicomViewer = (containerId: string) => { mountDicomViewers({ force: true, containerIds: [containerId] }); -}; \ No newline at end of file +}; +window.setDicomViewerLogging = (config) => { + setLoggerConfig({ + debug: config.debug, + format: config.format ? parseLogFormat(config.format) : undefined, + }); +}; +window.getDicomViewerLogging = () => getLoggerConfig(); diff --git a/dicom-viewer/src/nonDicomTest.tsx b/dicom-viewer/src/nonDicomTest.tsx index 6d4aad5..7655e6a 100644 --- a/dicom-viewer/src/nonDicomTest.tsx +++ b/dicom-viewer/src/nonDicomTest.tsx @@ -1,5 +1,8 @@ import React from 'react' 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. const sampleImages = [ @@ -38,7 +41,7 @@ function mount() { ) }).catch(err => { - console.error('Failed to load App for non-dicom test', err) + testLogger.error('Failed to load App for non-dicom test', err) }) }