Compare commits
24
Commits
1674254a3c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f323dfd559 | ||
|
|
0c4da6e022 | ||
|
|
d0f3431ef4 | ||
|
|
b961a806d9 | ||
|
|
1ddc9a810a | ||
|
|
b2bd2791a5 | ||
|
|
b527eb1139 | ||
|
|
6a0552c843 | ||
|
|
a5c530a907 | ||
|
|
41f13ade5e | ||
|
|
b0d8c72636 | ||
|
|
4dd63668b0 | ||
|
|
77bbbd548e | ||
|
|
7c17a27ec3 | ||
|
|
68248737ec | ||
|
|
13361a30a5 | ||
|
|
627cd740f1 | ||
|
|
af6e22fd91 | ||
|
|
7b786fcf9f | ||
|
|
c9902a2e15 | ||
|
|
99df1cd1c1 | ||
|
|
f169238057 | ||
|
|
f0e0fe0ef6 | ||
|
|
19702b6052 |
@@ -0,0 +1,128 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Fluoro Multiframe Test</title>
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.dicom-viewer-root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
background: #222;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div
|
||||||
|
id="main_viewer"
|
||||||
|
class="dicom-viewer-root"
|
||||||
|
data-auto-cache-stack="false"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Provide noop React Refresh preamble to avoid plugin-react runtime errors -->
|
||||||
|
<script>
|
||||||
|
window.$RefreshReg$ = window.$RefreshReg$ || function () {};
|
||||||
|
window.$RefreshSig$ =
|
||||||
|
window.$RefreshSig$ ||
|
||||||
|
function () {
|
||||||
|
return function (type) {
|
||||||
|
return type;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Viewer state for fluoro multiframe images (using relative paths from public/test_images/)
|
||||||
|
const state = {
|
||||||
|
grid: { rows: 1, cols: 2 },
|
||||||
|
modes: ["stack", "stack"],
|
||||||
|
stacks: [
|
||||||
|
{
|
||||||
|
i: [
|
||||||
|
"wadouri:https://www.penracourses.org.uk/media/atlas/dicom/1.3.12.2.1107.5.4.5.180506.30000026031914043350300000126.4.dcm",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stackIdx: [0, 1],
|
||||||
|
stackPos: [0, 1],
|
||||||
|
props: [
|
||||||
|
{
|
||||||
|
voiRange: { lower: 0, upper: 500 },
|
||||||
|
invert: false,
|
||||||
|
interpolationType: 1,
|
||||||
|
VOILUTFunction: "LINEAR",
|
||||||
|
colormap: { name: "Grayscale", opacity: [] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
voiRange: { lower: 0, upper: 500 },
|
||||||
|
invert: false,
|
||||||
|
interpolationType: 1,
|
||||||
|
VOILUTFunction: "LINEAR",
|
||||||
|
colormap: { name: "Grayscale", opacity: [] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cam: [
|
||||||
|
{
|
||||||
|
viewUp: [0, 0, 1],
|
||||||
|
viewPlaneNormal: [1, 0, 0],
|
||||||
|
position: [150, 0, 0],
|
||||||
|
focalPoint: [0, 0, 0],
|
||||||
|
parallelProjection: true,
|
||||||
|
parallelScale: 100,
|
||||||
|
viewAngle: 90,
|
||||||
|
flipHorizontal: false,
|
||||||
|
flipVertical: false,
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewUp: [0, 0, 1],
|
||||||
|
viewPlaneNormal: [1, 0, 0],
|
||||||
|
position: [150, 0, 0],
|
||||||
|
focalPoint: [0, 0, 0],
|
||||||
|
parallelProjection: true,
|
||||||
|
parallelScale: 100,
|
||||||
|
viewAngle: 90,
|
||||||
|
flipHorizontal: false,
|
||||||
|
flipVertical: false,
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
orientations: [null, null],
|
||||||
|
volumeSliceIndices: [null, null],
|
||||||
|
};
|
||||||
|
|
||||||
|
function callWhenReady(fnName, payload) {
|
||||||
|
const maxWait = 5000;
|
||||||
|
const start = Date.now();
|
||||||
|
(function check() {
|
||||||
|
if (window[fnName] && typeof window[fnName] === "function") {
|
||||||
|
try {
|
||||||
|
window[fnName](payload);
|
||||||
|
console.log("imported viewer state via", fnName);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("importViewerState call failed", e);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Date.now() - start > maxWait) {
|
||||||
|
console.warn("Timed out waiting for", fnName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(check, 2000);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
callWhenReady("importViewerState", state);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because one or more lines are too long
@@ -23,6 +23,7 @@
|
|||||||
<li><a href="/load-dir-test.html">load-dir-test.html</a> — directory loading test</li>
|
<li><a href="/load-dir-test.html">load-dir-test.html</a> — directory loading test</li>
|
||||||
<li><a href="/non-dicom-test.html">non-dicom-test.html</a> — non-DICOM handling test</li>
|
<li><a href="/non-dicom-test.html">non-dicom-test.html</a> — non-DICOM handling test</li>
|
||||||
<li><a href="/index.html">index.html</a> — project index</li>
|
<li><a href="/index.html">index.html</a> — project index</li>
|
||||||
|
<li><a href="/fluoro-test.html">fluoro-test.html</a> — fluoro multiframe test</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>If you want these links added to the README, tell me and I will update it.</p>
|
<p>If you want these links added to the README, tell me and I will update it.</p>
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4923
-561
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,181 @@
|
|||||||
|
export type LogLevel = "debug" | "info" | "warn" | "error" | "silent";
|
||||||
|
export type LogFormat = "compact" | "verbose" | "json";
|
||||||
|
|
||||||
|
export type LoggerConfig = {
|
||||||
|
minLevel: LogLevel;
|
||||||
|
format: LogFormat;
|
||||||
|
namespace: string;
|
||||||
|
dedupeWindowMs: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LEVEL_ORDER: Record<LogLevel, number> = {
|
||||||
|
debug: 0,
|
||||||
|
info: 1,
|
||||||
|
warn: 2,
|
||||||
|
error: 3,
|
||||||
|
silent: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: LoggerConfig = {
|
||||||
|
minLevel: "warn",
|
||||||
|
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) {
|
||||||
|
return LEVEL_ORDER[level] >= LEVEL_ORDER[loggerConfig.minLevel];
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldDedupe(level: LogLevel) {
|
||||||
|
return level === "debug" || level === "info";
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse a log level string. Falls back to the default ("warn") for unknown values. */
|
||||||
|
export function parseLogLevel(value: unknown): LogLevel {
|
||||||
|
if (value === "debug" || value === "info" || value === "warn" || value === "error" || value === "silent") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return DEFAULT_CONFIG.minLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Backward-compat helper: maps a truthy debug flag to minLevel "debug" or "warn". */
|
||||||
|
export function parseDebugFlag(value: unknown): LogLevel {
|
||||||
|
if (typeof value === "boolean") {
|
||||||
|
return value ? "debug" : "warn";
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const normalized = value.trim().toLowerCase();
|
||||||
|
const truthy = normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
||||||
|
return truthy ? "debug" : "warn";
|
||||||
|
}
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return value !== 0 ? "debug" : "warn";
|
||||||
|
}
|
||||||
|
return "warn";
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }),
|
||||||
|
}),
|
||||||
|
};
|
||||||
+213
-78
@@ -1,10 +1,65 @@
|
|||||||
import React from 'react'
|
import React from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot, type Root } from "react-dom/client";
|
||||||
import ReactDOM from 'react-dom/client'
|
import App from "./App.tsx";
|
||||||
import App from './App.tsx'
|
import "./index.css";
|
||||||
import Nifti from './Nifti.tsx'
|
import { availableImageIds } from "./lib/createImageIds.ts";
|
||||||
import './index.css'
|
import {
|
||||||
import { createImageIds, availableImageIds } from "./lib/createImageIds.ts"
|
getLoggerConfig,
|
||||||
|
logger,
|
||||||
|
parseDebugFlag,
|
||||||
|
parseLogFormat,
|
||||||
|
parseLogLevel,
|
||||||
|
setLoggerConfig,
|
||||||
|
type LogFormat,
|
||||||
|
type LogLevel,
|
||||||
|
} from "./lib/logger";
|
||||||
|
|
||||||
|
type MountOptions = {
|
||||||
|
force?: boolean;
|
||||||
|
containerIds?: string[];
|
||||||
|
logging?: {
|
||||||
|
/** Minimum log level to emit. Takes precedence over `debug`. */
|
||||||
|
minLevel?: LogLevel;
|
||||||
|
/** @deprecated Use minLevel instead. true maps to "debug", false maps to "warn". */
|
||||||
|
debug?: boolean;
|
||||||
|
format?: LogFormat;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type ViewerDescriptor = {
|
||||||
|
imageIds: string[];
|
||||||
|
name?: string;
|
||||||
|
caseId?: string;
|
||||||
|
studyId?: string;
|
||||||
|
series?: Array<{
|
||||||
|
key?: string;
|
||||||
|
label?: string;
|
||||||
|
imageIds?: string[];
|
||||||
|
i?: string[];
|
||||||
|
name?: string;
|
||||||
|
seriesId?: string | number;
|
||||||
|
seriesInstanceUID?: string;
|
||||||
|
bValue?: number | null;
|
||||||
|
groupValues?: Record<string, string>;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RootRecord = {
|
||||||
|
root: Root;
|
||||||
|
signature: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const rootRegistry = new Map<HTMLElement, RootRecord>();
|
||||||
|
const mainLogger = logger.child("main");
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
mountDicomViewers?: (options?: MountOptions) => void;
|
||||||
|
remountDicomViewer?: (containerId: string) => void;
|
||||||
|
setDicomViewerLogging?: (config: { minLevel?: LogLevel; debug?: boolean; format?: LogFormat }) => void;
|
||||||
|
getDicomViewerLogging?: () => { minLevel: LogLevel; format: LogFormat; namespace: string; dedupeWindowMs: number };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Normalize incoming image ids/URLs and ensure DICOM entries use a valid wadouri: prefix.
|
// Normalize incoming image ids/URLs and ensure DICOM entries use a valid wadouri: prefix.
|
||||||
const normalizeImageId = (value: string) => {
|
const normalizeImageId = (value: string) => {
|
||||||
@@ -35,57 +90,32 @@ const toDescriptor = (arr: string[] | undefined) => {
|
|||||||
return { imageIds: toWadouri(arr) };
|
return { imageIds: toWadouri(arr) };
|
||||||
};
|
};
|
||||||
|
|
||||||
export function mountDicomViewers() {
|
const parseToDescriptors = (parsed: any): Array<ViewerDescriptor> => {
|
||||||
// Test containers
|
|
||||||
const test_containers = document.querySelectorAll(".dicom-viewer-test-root");
|
|
||||||
test_containers.forEach((container) => {
|
|
||||||
const id = container.id || '';
|
|
||||||
const imageStacks = () => availableImageIds();
|
|
||||||
createRoot(container).render(
|
|
||||||
<App
|
|
||||||
container_id={id}
|
|
||||||
imageStacks={imageStacks}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Main containers
|
|
||||||
const containers = document.querySelectorAll(".dicom-viewer-root");
|
|
||||||
containers.forEach((container) => {
|
|
||||||
const id = container.id || '';
|
|
||||||
let imageStacks: () => Promise<any[]>;
|
|
||||||
const isEmpty = container.getAttribute('data-empty') === 'true';
|
|
||||||
|
|
||||||
// New: support data-named-stacks (richer format) and fallback to data-images
|
|
||||||
const dataNamed = container.getAttribute('data-named-stacks');
|
|
||||||
const dataImages = container.getAttribute('data-images');
|
|
||||||
|
|
||||||
const parseToDescriptors = (parsed: any): Array<any> => {
|
|
||||||
// Return array of descriptors: either simple string[] => { imageIds: string[] }
|
|
||||||
// or objects: { imageIds: string[], name?, caseId?, studyId? }
|
|
||||||
if (!parsed) return [];
|
if (!parsed) return [];
|
||||||
|
|
||||||
// If top-level is an array of strings -> single unnamed stack
|
|
||||||
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") {
|
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") {
|
||||||
return [toDescriptor(parsed)];
|
return [toDescriptor(parsed)];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(parsed)) {
|
if (!Array.isArray(parsed)) {
|
||||||
const out: any[] = [];
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const out: Array<ViewerDescriptor> = [];
|
||||||
|
|
||||||
parsed.forEach((item, itemIdx) => {
|
parsed.forEach((item, itemIdx) => {
|
||||||
// Case object with nested stacks: { caseId, studyId, stacks: [ { name, imageIds } ] }
|
|
||||||
if (item && typeof item === "object" && Array.isArray(item.stacks)) {
|
if (item && typeof item === "object" && Array.isArray(item.stacks)) {
|
||||||
const caseId = item.caseId || item.caseUID || item.case || undefined;
|
const caseId = item.caseId || item.caseUID || item.case || undefined;
|
||||||
const studyId = item.studyId || item.studyUID || item.studyInstanceUID || undefined;
|
const studyId = item.studyId || item.studyUID || item.studyInstanceUID || undefined;
|
||||||
|
|
||||||
item.stacks.forEach((s: any, sIdx: number) => {
|
item.stacks.forEach((s: any, sIdx: number) => {
|
||||||
let imageIds: string[] = [];
|
let imageIds: string[] = [];
|
||||||
if (Array.isArray(s)) imageIds = s;
|
if (Array.isArray(s)) imageIds = s;
|
||||||
else if (Array.isArray(s.imageIds)) imageIds = s.imageIds;
|
else if (Array.isArray(s.imageIds)) imageIds = s.imageIds;
|
||||||
else if (Array.isArray(s.i)) imageIds = s.i;
|
else if (Array.isArray(s.i)) imageIds = s.i;
|
||||||
|
|
||||||
// Validation
|
if (!Array.isArray(imageIds) || imageIds.length === 0 || !imageIds.every((x) => typeof x === "string")) {
|
||||||
if (!Array.isArray(imageIds) || imageIds.length === 0 || !imageIds.every(x => typeof x === "string")) {
|
mainLogger.warn(`data-named-stacks: skipping invalid nested stack at parsed[${itemIdx}].stacks[${sIdx}]`, s);
|
||||||
console.warn(`data-named-stacks: skipping invalid nested stack at parsed[${itemIdx}].stacks[${sIdx}]`, s);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,17 +124,16 @@ export function mountDicomViewers() {
|
|||||||
name: s.name || s.label || undefined,
|
name: s.name || s.label || undefined,
|
||||||
caseId,
|
caseId,
|
||||||
studyId: s.studyId || studyId,
|
studyId: s.studyId || studyId,
|
||||||
|
series: Array.isArray(s.series) ? s.series : undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stack object: { imageIds, i, name, caseId, studyId }
|
if (item && typeof item === "object" && (Array.isArray(item.imageIds) || Array.isArray(item.i))) {
|
||||||
if (item && typeof item === 'object' && (Array.isArray(item.imageIds) || Array.isArray(item.i))) {
|
|
||||||
const imageIds = Array.isArray(item.imageIds) ? item.imageIds : item.i;
|
const imageIds = Array.isArray(item.imageIds) ? item.imageIds : item.i;
|
||||||
|
if (!Array.isArray(imageIds) || imageIds.length === 0 || !imageIds.every((x) => typeof x === "string")) {
|
||||||
if (!Array.isArray(imageIds) || imageIds.length === 0 || !imageIds.every(x => typeof x === "string")) {
|
mainLogger.warn(`data-named-stacks: skipping invalid stack object at parsed[${itemIdx}]`, item);
|
||||||
console.warn(`data-named-stacks: skipping invalid stack object at parsed[${itemIdx}]`, item);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,25 +142,137 @@ export function mountDicomViewers() {
|
|||||||
name: item.name || item.label || undefined,
|
name: item.name || item.label || undefined,
|
||||||
caseId: item.caseId || item.caseUID || undefined,
|
caseId: item.caseId || item.caseUID || undefined,
|
||||||
studyId: item.studyId || item.studyUID || item.studyInstanceUID || undefined,
|
studyId: item.studyId || item.studyUID || item.studyInstanceUID || undefined,
|
||||||
|
series: Array.isArray(item.series) ? item.series : undefined,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plain array (stack)
|
|
||||||
if (Array.isArray(item) && item.length > 0 && typeof item[0] === "string") {
|
if (Array.isArray(item) && item.length > 0 && typeof item[0] === "string") {
|
||||||
out.push(toDescriptor(item));
|
out.push(toDescriptor(item));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown / invalid entry
|
mainLogger.warn(`data-named-stacks: unknown or invalid entry at parsed[${itemIdx}] - skipping`, item);
|
||||||
console.warn(`data-named-stacks: unknown or invalid entry at parsed[${itemIdx}] — skipping`, item);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
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") || "",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldMountContainer = (container: HTMLElement, options?: MountOptions): boolean => {
|
||||||
|
if (!options?.containerIds || options.containerIds.length === 0) return true;
|
||||||
|
return options.containerIds.includes(container.id || "");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getContainerLogConfig = (container: HTMLElement, options?: MountOptions) => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const urlDebug = urlParams.get("debug");
|
||||||
|
const localSyncDebug = typeof window !== "undefined" && window.localStorage ? window.localStorage.getItem("multiuser_sync_debug_mode") : null;
|
||||||
|
|
||||||
|
const attrLevel = container.getAttribute("data-log-level");
|
||||||
|
const attrDebug = container.getAttribute("data-log-debug");
|
||||||
|
const attrFormat = container.getAttribute("data-log-format");
|
||||||
|
|
||||||
|
// Resolve minLevel: LocalStorage debug > URL debug parameter > data-log-level > options.minLevel > data-log-debug (legacy)
|
||||||
|
let minLevel: LogLevel;
|
||||||
|
if (localSyncDebug === "true") {
|
||||||
|
minLevel = "debug";
|
||||||
|
} else if (urlDebug !== null) {
|
||||||
|
minLevel = parseDebugFlag(urlDebug);
|
||||||
|
} else if (attrLevel !== null) {
|
||||||
|
minLevel = parseLogLevel(attrLevel);
|
||||||
|
} else if (options?.logging?.minLevel) {
|
||||||
|
minLevel = options.logging.minLevel;
|
||||||
|
} else if (attrDebug !== null) {
|
||||||
|
minLevel = parseDebugFlag(attrDebug);
|
||||||
|
} else if (options?.logging?.debug !== undefined) {
|
||||||
|
minLevel = parseDebugFlag(options.logging.debug);
|
||||||
|
} else {
|
||||||
|
minLevel = "warn";
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
const format = parseLogFormat(attrFormat ?? options?.logging?.format);
|
||||||
|
return { minLevel, 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
existing.root.unmount();
|
||||||
|
rootRegistry.delete(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoggerConfig(initialLoggerConfig);
|
||||||
|
|
||||||
|
const root = createRoot(container);
|
||||||
|
root.render(
|
||||||
|
<App
|
||||||
|
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}
|
||||||
|
initialLoggerConfig={initialLoggerConfig}
|
||||||
|
multiuserRoom={container.getAttribute("data-multiuser-room") || undefined}
|
||||||
|
username={container.getAttribute("data-username") || undefined}
|
||||||
|
isStaff={container.getAttribute("data-is-staff") === "true"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
rootRegistry.set(container, { root, signature });
|
||||||
|
};
|
||||||
|
|
||||||
|
export function mountDicomViewers(options?: MountOptions) {
|
||||||
|
// Clean up any stale roots from elements that have been removed from the DOM
|
||||||
|
for (const [container, entry] of rootRegistry.entries()) {
|
||||||
|
if (!document.body.contains(container)) {
|
||||||
|
try {
|
||||||
|
mainLogger.info("Unmounting stale React root for container that was removed from DOM:", container.id);
|
||||||
|
entry.root.unmount();
|
||||||
|
} catch (e) {
|
||||||
|
mainLogger.warn("Failed to unmount stale React root", e);
|
||||||
|
}
|
||||||
|
rootRegistry.delete(container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test containers
|
||||||
|
const testContainers = document.querySelectorAll(".dicom-viewer-test-root");
|
||||||
|
testContainers.forEach((container) => {
|
||||||
|
if (!shouldMountContainer(container as HTMLElement, options)) return;
|
||||||
|
const imageStacks = () => availableImageIds();
|
||||||
|
mountContainer(container as HTMLElement, imageStacks, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Main containers
|
||||||
|
const containers = document.querySelectorAll(".dicom-viewer-root");
|
||||||
|
containers.forEach((container) => {
|
||||||
|
if (!shouldMountContainer(container as HTMLElement, options)) return;
|
||||||
|
|
||||||
|
let imageStacks: () => Promise<any[]>;
|
||||||
|
const isEmpty = container.getAttribute("data-empty") === "true";
|
||||||
|
|
||||||
|
const dataNamed = container.getAttribute("data-named-stacks");
|
||||||
|
const dataImages = container.getAttribute("data-images");
|
||||||
|
|
||||||
if (isEmpty) {
|
if (isEmpty) {
|
||||||
imageStacks = () => Promise.resolve([] as any[]);
|
imageStacks = () => Promise.resolve([] as any[]);
|
||||||
} else if (dataNamed) {
|
} else if (dataNamed) {
|
||||||
@@ -139,7 +280,7 @@ export function mountDicomViewers() {
|
|||||||
try {
|
try {
|
||||||
parsed = JSON.parse(dataNamed);
|
parsed = JSON.parse(dataNamed);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Invalid data-named-stacks JSON:", e);
|
mainLogger.error("Invalid data-named-stacks JSON:", e);
|
||||||
parsed = [];
|
parsed = [];
|
||||||
}
|
}
|
||||||
const descriptors = parseToDescriptors(parsed);
|
const descriptors = parseToDescriptors(parsed);
|
||||||
@@ -149,46 +290,40 @@ export function mountDicomViewers() {
|
|||||||
try {
|
try {
|
||||||
parsed = JSON.parse(dataImages);
|
parsed = JSON.parse(dataImages);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Invalid data-images JSON:", e);
|
mainLogger.error("Invalid data-images JSON:", e);
|
||||||
parsed = [];
|
parsed = [];
|
||||||
}
|
}
|
||||||
// Keep old behavior for data-images
|
|
||||||
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") {
|
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") {
|
||||||
imageStacks = () => Promise.resolve([toWadouri(parsed)]);
|
imageStacks = () => Promise.resolve([toWadouri(parsed)]);
|
||||||
} else if (Array.isArray(parsed)) {
|
} else if (Array.isArray(parsed)) {
|
||||||
imageStacks = () => Promise.resolve(parsed.map(
|
imageStacks = () =>
|
||||||
stack => Array.isArray(stack) ? toWadouri(stack) : []
|
Promise.resolve(parsed.map((stack) => (Array.isArray(stack) ? toWadouri(stack) : [])));
|
||||||
));
|
|
||||||
} else {
|
} else {
|
||||||
imageStacks = () => Promise.resolve([[]]);
|
imageStacks = () => Promise.resolve([[]]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No data provided: return no stacks (empty array) so viewer starts empty
|
|
||||||
imageStacks = () => Promise.resolve([] as any[]);
|
imageStacks = () => Promise.resolve([] as any[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const autoCacheStackAttr = container.getAttribute('data-auto-cache-stack');
|
mountContainer(container as HTMLElement, imageStacks, options);
|
||||||
const autoCacheStack = autoCacheStackAttr === "true" || autoCacheStackAttr === "1";
|
|
||||||
|
|
||||||
const annotationJson = container.getAttribute('data-annotationjson');
|
|
||||||
const viewerState = container.getAttribute('data-viewerstate');
|
|
||||||
console.log("Mounting DICOM viewer:")
|
|
||||||
console.log(viewerState, annotationJson, autoCacheStack);
|
|
||||||
|
|
||||||
createRoot(container).render(
|
|
||||||
<App
|
|
||||||
container_id={id}
|
|
||||||
imageStacks={imageStacks}
|
|
||||||
autoCacheStack={autoCacheStack}
|
|
||||||
annotationJson={annotationJson ? annotationJson : undefined}
|
|
||||||
viewerState={viewerState ? viewerState : undefined}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally, call it immediately for static containers:
|
|
||||||
mountDicomViewers();
|
mountDicomViewers();
|
||||||
|
|
||||||
// Optionally, expose globally for dynamic use:
|
window.mountDicomViewers = mountDicomViewers;
|
||||||
(window as any).mountDicomViewers = mountDicomViewers;
|
window.remountDicomViewer = (containerId: string) => {
|
||||||
|
mountDicomViewers({ force: true, containerIds: [containerId] });
|
||||||
|
};
|
||||||
|
window.setDicomViewerLogging = (config) => {
|
||||||
|
const minLevel = config.minLevel
|
||||||
|
? parseLogLevel(config.minLevel)
|
||||||
|
: config.debug !== undefined
|
||||||
|
? parseDebugFlag(config.debug)
|
||||||
|
: undefined;
|
||||||
|
setLoggerConfig({
|
||||||
|
minLevel,
|
||||||
|
format: config.format ? parseLogFormat(config.format) : undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
window.getDicomViewerLogging = () => getLoggerConfig();
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { logger } from './lib/logger'
|
||||||
|
|
||||||
|
const testLogger = logger.child('nonDicomTest')
|
||||||
|
|
||||||
// Sample external images (use CORS-friendly URLs). These are Wikimedia Commons images.
|
// Sample external images (use CORS-friendly URLs). These are Wikimedia Commons images.
|
||||||
const sampleImages = [
|
const sampleImages = [
|
||||||
@@ -38,7 +41,7 @@ function mount() {
|
|||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error('Failed to load App for non-dicom test', err)
|
testLogger.error('Failed to load App for non-dicom test', err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user