Add stripWadouriPrefix function and refactor image ID handling for improved URL normalization
This commit is contained in:
+124
-34
@@ -166,6 +166,12 @@ function getToolIcon(toolName?: string) {
|
||||
return found?.icon || '🔖';
|
||||
}
|
||||
|
||||
function stripWadouriPrefix(imageId?: string): string {
|
||||
if (!imageId) return "";
|
||||
if (!imageId.startsWith("wadouri:")) return imageId;
|
||||
return imageId.replace(/^wadouri:+/, "").replace(/^:+/, "");
|
||||
}
|
||||
|
||||
// Mouse button labels
|
||||
const MOUSE_BUTTONS = [
|
||||
{ label: "Left", value: "Primary" },
|
||||
@@ -601,7 +607,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
try {
|
||||
const first = imageIds[0];
|
||||
if (typeof fetch === 'function' && typeof (window as any).dicomParser !== 'undefined' && first.startsWith('wadouri:')) {
|
||||
const url = first.slice(7);
|
||||
const url = stripWadouriPrefix(first);
|
||||
const resp = await fetch(url);
|
||||
const buffer = await resp.arrayBuffer();
|
||||
const ds = (window as any).dicomParser.parseDicom(new Uint8Array(buffer));
|
||||
@@ -908,7 +914,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
for (const imageId of imageIds) {
|
||||
if (imageId.startsWith("wadouri:")) {
|
||||
try {
|
||||
const url = imageId.slice(8);
|
||||
const url = stripWadouriPrefix(imageId);
|
||||
if (cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager) {
|
||||
// @ts-ignore
|
||||
const result = cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.load(url);
|
||||
@@ -1018,11 +1024,11 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
() => Array(MAX_VIEWPORTS).fill(null).map(() => [])
|
||||
);
|
||||
const viewportStackSignaturesRef = useRef<string[]>(Array(MAX_VIEWPORTS).fill(""));
|
||||
const defaultVoiRangeByViewportRef = useRef<Array<{ lower: number; upper: number } | null>>(Array(MAX_VIEWPORTS).fill(null));
|
||||
|
||||
const [viewportModes, setViewportModes] = useState<Array<'stack' | 'volume'>>(
|
||||
() => Array(MAX_VIEWPORTS).fill('stack')
|
||||
);
|
||||
|
||||
// State for preset menu open/close per viewport
|
||||
const [presetsOpenArr, setPresetsOpenArr] = useState<boolean[]>(
|
||||
() => Array(viewportGrid.rows * viewportGrid.cols).fill(false)
|
||||
@@ -1675,7 +1681,27 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
const currentImageId = imageIds[currentIndex];
|
||||
if (currentImageId) {
|
||||
const seriesMeta = metaData.get("generalSeriesModule", currentImageId) as any;
|
||||
const planeMeta = metaData.get("imagePlaneModule", currentImageId) as any;
|
||||
const pixelMeta = metaData.get("imagePixelModule", currentImageId) as any;
|
||||
modality = seriesMeta?.modality || "";
|
||||
if (pixelMeta?.rows && pixelMeta?.columns) {
|
||||
resolution = `${pixelMeta.columns} x ${pixelMeta.rows}`;
|
||||
}
|
||||
const spacingParts = [
|
||||
planeMeta?.columnPixelSpacing,
|
||||
planeMeta?.rowPixelSpacing,
|
||||
planeMeta?.sliceThickness,
|
||||
].filter((value) => value !== undefined && value !== null && value !== "");
|
||||
if (spacingParts.length > 0) {
|
||||
spacing = spacingParts.join(" x ");
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof viewport.getCamera === "function") {
|
||||
const camera = viewport.getCamera();
|
||||
if (camera?.focalPoint && Array.isArray(camera.focalPoint)) {
|
||||
focalPoint = camera.focalPoint.map((value: number) => value.toFixed(1)).join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
if (currentIndex >= 0) {
|
||||
@@ -1697,6 +1723,9 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
const { lower, upper } = props.voiRange;
|
||||
windowCenter = ((upper + lower) / 2).toFixed(0);
|
||||
windowWidth = (upper - lower).toFixed(0);
|
||||
if (defaultVoiRangeByViewportRef.current[viewportIdx] === null) {
|
||||
defaultVoiRangeByViewportRef.current[viewportIdx] = { lower, upper };
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (viewportModes[viewportIdx] === "volume") {
|
||||
@@ -1721,6 +1750,9 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
const { lower, upper } = props.voiRange;
|
||||
windowCenter = ((upper + lower) / 2).toFixed(0);
|
||||
windowWidth = (upper - lower).toFixed(0);
|
||||
if (defaultVoiRangeByViewportRef.current[viewportIdx] === null) {
|
||||
defaultVoiRangeByViewportRef.current[viewportIdx] = { lower, upper };
|
||||
}
|
||||
}
|
||||
}
|
||||
// Try to get slice position in mm (distance along viewPlaneNormal from origin)
|
||||
@@ -1787,6 +1819,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
const signature = `${ids.length}:${ids[0] || ""}:${ids[ids.length - 1] || ""}`;
|
||||
if (viewportStackSignaturesRef.current[i] !== signature) {
|
||||
viewportStackSignaturesRef.current[i] = signature;
|
||||
defaultVoiRangeByViewportRef.current[i] = null;
|
||||
next[i] = [];
|
||||
changed = true;
|
||||
}
|
||||
@@ -2290,6 +2323,52 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
}
|
||||
}, [updateOverlay]);
|
||||
|
||||
const applyDefaultWindowLevel = useCallback((viewportIdx: number, resetCamera = false) => {
|
||||
const viewportId = `CT_${viewportIdx}`;
|
||||
try {
|
||||
const renderingEngine = renderingEngineRef.current;
|
||||
const viewport = renderingEngine?.getViewport(viewportId);
|
||||
if (!viewport) return false;
|
||||
|
||||
const ids = typeof viewport.getImageIds === "function"
|
||||
? viewport.getImageIds()
|
||||
: (viewportImageIds[viewportIdx] || []);
|
||||
const currentIndex = typeof viewport.getCurrentImageIdIndex === "function"
|
||||
? viewport.getCurrentImageIdIndex()
|
||||
: 0;
|
||||
const imageId = ids[currentIndex] || ids[0];
|
||||
const defaultVoi = defaultVoiRangeByViewportRef.current[viewportIdx];
|
||||
const voiMeta = imageId ? (metaData.get("voiLutModule", imageId) || {}) as any : {};
|
||||
const centerMeta = Array.isArray(voiMeta.windowCenter) ? voiMeta.windowCenter[0] : voiMeta.windowCenter;
|
||||
const widthMeta = Array.isArray(voiMeta.windowWidth) ? voiMeta.windowWidth[0] : voiMeta.windowWidth;
|
||||
|
||||
if (defaultVoi) {
|
||||
viewport.setProperties({ voiRange: defaultVoi });
|
||||
} else if (Number.isFinite(Number(centerMeta)) && Number.isFinite(Number(widthMeta)) && Number(widthMeta) > 0) {
|
||||
const center = Number(centerMeta);
|
||||
const width = Number(widthMeta);
|
||||
viewport.setProperties({
|
||||
voiRange: {
|
||||
lower: center - width / 2,
|
||||
upper: center + width / 2,
|
||||
},
|
||||
});
|
||||
} else if (typeof viewport.resetProperties === "function") {
|
||||
viewport.resetProperties();
|
||||
}
|
||||
|
||||
if (resetCamera && typeof viewport.resetCamera === "function") {
|
||||
viewport.resetCamera();
|
||||
}
|
||||
viewport.render();
|
||||
updateOverlay(viewportIdx, viewport);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn("Failed to apply default window level", err);
|
||||
return false;
|
||||
}
|
||||
}, [updateOverlay, viewportImageIds]);
|
||||
|
||||
const handleAutoLevel = useCallback((viewportIdx: number) => {
|
||||
const viewportId = `CT_${viewportIdx}`;
|
||||
try {
|
||||
@@ -2307,39 +2386,51 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
const imageId = ids[currentIndex] || ids[0];
|
||||
|
||||
if (!imageId) {
|
||||
if (typeof viewport.resetProperties === "function") viewport.resetProperties();
|
||||
viewport.render();
|
||||
updateOverlay(viewportIdx, viewport);
|
||||
applyDefaultWindowLevel(viewportIdx);
|
||||
return;
|
||||
}
|
||||
|
||||
const pixelMeta = (metaData.get("imagePixelModule", imageId) || {}) as any;
|
||||
const lutMeta = (metaData.get("modalityLutModule", imageId) || {}) as any;
|
||||
const voiMeta = (metaData.get("voiLutModule", imageId) || {}) as any;
|
||||
const defaultVoi = defaultVoiRangeByViewportRef.current[viewportIdx];
|
||||
|
||||
const minRaw = Number(pixelMeta.smallestPixelValue ?? pixelMeta.smallestImagePixelValue);
|
||||
const maxRaw = Number(pixelMeta.largestPixelValue ?? pixelMeta.largestImagePixelValue);
|
||||
const slope = Number(lutMeta.rescaleSlope ?? 1);
|
||||
const intercept = Number(lutMeta.rescaleIntercept ?? 0);
|
||||
const centerMeta = Array.isArray(voiMeta.windowCenter) ? voiMeta.windowCenter[0] : voiMeta.windowCenter;
|
||||
const widthMeta = Array.isArray(voiMeta.windowWidth) ? voiMeta.windowWidth[0] : voiMeta.windowWidth;
|
||||
|
||||
if (!Number.isFinite(minRaw) || !Number.isFinite(maxRaw) || maxRaw <= minRaw) {
|
||||
if (typeof viewport.resetProperties === "function") viewport.resetProperties();
|
||||
viewport.render();
|
||||
updateOverlay(viewportIdx, viewport);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Number.isFinite(minRaw) && Number.isFinite(maxRaw) && maxRaw > minRaw) {
|
||||
viewport.setProperties({
|
||||
voiRange: {
|
||||
lower: minRaw * slope + intercept,
|
||||
upper: maxRaw * slope + intercept,
|
||||
},
|
||||
});
|
||||
} else if (Number.isFinite(Number(centerMeta)) && Number.isFinite(Number(widthMeta)) && Number(widthMeta) > 0) {
|
||||
const center = Number(centerMeta);
|
||||
const width = Number(widthMeta);
|
||||
viewport.setProperties({
|
||||
voiRange: {
|
||||
lower: center - width / 2,
|
||||
upper: center + width / 2,
|
||||
},
|
||||
});
|
||||
} else if (defaultVoi) {
|
||||
viewport.setProperties({ voiRange: defaultVoi });
|
||||
} else {
|
||||
applyDefaultWindowLevel(viewportIdx);
|
||||
return;
|
||||
}
|
||||
|
||||
viewport.render();
|
||||
updateOverlay(viewportIdx, viewport);
|
||||
} catch (e) {
|
||||
// Ignore failures and keep existing VOI.
|
||||
applyDefaultWindowLevel(viewportIdx);
|
||||
}
|
||||
}, [updateOverlay, viewportImageIds]);
|
||||
}, [applyDefaultWindowLevel, updateOverlay, viewportImageIds]);
|
||||
|
||||
const toggleViewerFullscreen = useCallback(() => {
|
||||
const root = appRootRef.current;
|
||||
@@ -2461,7 +2552,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
try {
|
||||
const firstId = wadouriImageIds[0];
|
||||
if (firstId && firstId.startsWith("wadouri:")) {
|
||||
const url = firstId.slice(8);
|
||||
const url = stripWadouriPrefix(firstId);
|
||||
const response = await fetch(url);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
||||
@@ -2686,8 +2777,8 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
if (!viewport || typeof viewport.getImageIds !== "function") return;
|
||||
const imageIds = viewport.getImageIds();
|
||||
// Some stacks may have imageIds with or without "wadouri:" prefix, so normalize for comparison
|
||||
const normImageIds = imageIds.map(id => id.replace(/^wadouri:/, ""));
|
||||
const normTarget = imageId.replace(/^wadouri:/, "");
|
||||
const normImageIds = imageIds.map(id => stripWadouriPrefix(id));
|
||||
const normTarget = stripWadouriPrefix(imageId);
|
||||
const idx = normImageIds.indexOf(normTarget);
|
||||
if (idx !== -1 && typeof viewport.setImageIdIndex === "function") {
|
||||
csUtilities.jumpToSlice(viewport.element, { imageIndex: idx });
|
||||
@@ -2720,7 +2811,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
let uid = studyInstanceUID;
|
||||
if (!uid && imageIds[0]?.startsWith("wadouri:")) {
|
||||
try {
|
||||
const url = imageIds[0].slice(8);
|
||||
const url = stripWadouriPrefix(imageIds[0]);
|
||||
const response = await fetch(url);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
||||
@@ -3790,6 +3881,14 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
<div>
|
||||
WC: {imageInfos[i]?.windowCenter} WW: {imageInfos[i]?.windowWidth}
|
||||
</div>
|
||||
{volumeDiagnosticsEnabled && (
|
||||
<>
|
||||
<div>Ori: {imageInfos[i]?.orientation || "N/A"}</div>
|
||||
{imageInfos[i]?.resolution && <div>Res: {imageInfos[i]?.resolution}</div>}
|
||||
{imageInfos[i]?.spacing && <div>Spacing: {imageInfos[i]?.spacing}</div>}
|
||||
{imageInfos[i]?.focalPoint && <div>Pos: {imageInfos[i]?.focalPoint}</div>}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
@@ -3907,19 +4006,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
onClick={e => {
|
||||
e.stopPropagation(); // <-- prevent bubbling to viewport
|
||||
if (preset.action === "reset") {
|
||||
try {
|
||||
const viewportId = `CT_${i}`;
|
||||
const renderingEngine = renderingEngineRef.current;
|
||||
const viewport = renderingEngine?.getViewport(viewportId);
|
||||
if (viewport) {
|
||||
if (typeof viewport.resetProperties === "function") viewport.resetProperties();
|
||||
if (typeof viewport.resetCamera === "function") viewport.resetCamera();
|
||||
if (typeof viewport.render === "function") viewport.render();
|
||||
updateOverlay(i, viewport);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Failed to reset viewport properties", err);
|
||||
}
|
||||
applyDefaultWindowLevel(i, true);
|
||||
} else if (preset.action === "autolevel") {
|
||||
handleAutoLevel(i);
|
||||
} else {
|
||||
@@ -4722,6 +4809,9 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
width: "auto",
|
||||
pointerEvents: "auto",
|
||||
transition: "top 180ms ease",
|
||||
paddingTop: 28,
|
||||
marginTop: -28,
|
||||
paddingBottom: 12,
|
||||
}}
|
||||
onMouseEnter={() => setTopControlsPeek(true)}
|
||||
onMouseLeave={() => {
|
||||
@@ -4752,7 +4842,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 16, lineHeight: 1 }}>⛶</span>
|
||||
<span>Viewer</span>
|
||||
<span>Fullscreen</span>
|
||||
</button>
|
||||
|
||||
<div style={{ position: "relative", display: "flex", flexDirection: "column", alignItems: "stretch" }}>
|
||||
@@ -4993,7 +5083,7 @@ function App({ container_id, imageStacks, autoCacheStack, annotationJson, viewer
|
||||
const midIndex = Math.floor(imageIds.length / 2);
|
||||
const chosenImage = imageIds[midIndex] || imageIds[0];
|
||||
// Remove the wadouri: scheme if present. Use replace to avoid off-by-one slice errors.
|
||||
const thumbnailUrl = chosenImage ? chosenImage.replace(/^wadouri:/, '') : null;
|
||||
const thumbnailUrl = chosenImage ? stripWadouriPrefix(chosenImage) : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user