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:
+166
-89
@@ -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<HTMLDivElement>(null);
|
||||
const elementRef = useRef<HTMLDivElement>(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<LogFormat>(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<any>[] = [];
|
||||
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 <img> failed to load for stack', idx, src);
|
||||
appLogger.warn('Thumbnail <img> failed to load for stack', idx, src);
|
||||
const usedGeneratedImage = !!generatedValid;
|
||||
|
||||
if (usedGeneratedImage) {
|
||||
@@ -6469,6 +6514,38 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
</div>
|
||||
{/* Settings content: Tool dropdowns */}
|
||||
<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>
|
||||
{MOUSE_BUTTONS.map((btn) => (
|
||||
<div key={btn.value} style={{ marginBottom: 10 }}>
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }),
|
||||
}),
|
||||
};
|
||||
+70
-38
@@ -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<HTMLElement, RootRecord>();
|
||||
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<ViewerDescriptor> => {
|
||||
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<ViewerDescriptor> => {
|
||||
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<ViewerDescriptor> => {
|
||||
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<ViewerDescriptor> => {
|
||||
|
||||
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<any[]>, 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<any[]
|
||||
rootRegistry.delete(container);
|
||||
}
|
||||
|
||||
setLoggerConfig(initialLoggerConfig);
|
||||
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<App
|
||||
container_id={container.id || ''}
|
||||
container_id={container.id || ""}
|
||||
imageStacks={imageStacks}
|
||||
autoCacheStack={(container.getAttribute('data-auto-cache-stack') === "true" || container.getAttribute('data-auto-cache-stack') === "1")}
|
||||
annotationJson={container.getAttribute('data-annotationjson') || undefined}
|
||||
viewerState={container.getAttribute('data-viewerstate') || undefined}
|
||||
autoCacheStack={container.getAttribute("data-auto-cache-stack") === "true" || container.getAttribute("data-auto-cache-stack") === "1"}
|
||||
annotationJson={container.getAttribute("data-annotationjson") || 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) {
|
||||
// Test containers
|
||||
const test_containers = document.querySelectorAll(".dicom-viewer-test-root");
|
||||
test_containers.forEach((container) => {
|
||||
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<any[]>;
|
||||
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] });
|
||||
};
|
||||
};
|
||||
window.setDicomViewerLogging = (config) => {
|
||||
setLoggerConfig({
|
||||
debug: config.debug,
|
||||
format: config.format ? parseLogFormat(config.format) : undefined,
|
||||
});
|
||||
};
|
||||
window.getDicomViewerLogging = () => getLoggerConfig();
|
||||
|
||||
@@ -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() {
|
||||
</React.StrictMode>
|
||||
)
|
||||
}).catch(err => {
|
||||
console.error('Failed to load App for non-dicom test', err)
|
||||
testLogger.error('Failed to load App for non-dicom test', err)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user