Merge branch 'v3-stable' into customizable-TS

This commit is contained in:
Salim Kanoun 2023-05-29 23:26:36 +02:00 committed by GitHub
commit 56633a67d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1039 additions and 409 deletions

View File

@ -7,12 +7,13 @@ async function checkAndLoadContourData(instance, datasource) {
return Promise.reject('Invalid instance object or ROIContourSequence');
}
const promises = [];
let counter = 0;
const promisesMap = new Map();
for (const ROIContour of instance.ROIContourSequence) {
const referencedROINumber = ROIContour.ReferencedROINumber;
if (!ROIContour || !ROIContour.ContourSequence) {
return Promise.reject('Invalid ROIContour or ContourSequence');
promisesMap.set(referencedROINumber, [Promise.resolve([])]);
continue;
}
for (const Contour of ROIContour.ContourSequence) {
@ -21,9 +22,15 @@ async function checkAndLoadContourData(instance, datasource) {
}
const contourData = Contour.ContourData;
counter++;
if (Array.isArray(contourData)) {
promises.push(Promise.resolve(contourData));
promisesMap.has(referencedROINumber)
? promisesMap
.get(referencedROINumber)
.push(Promise.resolve(contourData))
: promisesMap.set(referencedROINumber, [
Promise.resolve(contourData),
]);
} else if (contourData && contourData.BulkDataURI) {
const bulkDataURI = contourData.BulkDataURI;
@ -44,53 +51,59 @@ async function checkAndLoadContourData(instance, datasource) {
SOPInstanceUID: instance.SOPInstanceUID,
});
promises.push(bulkDataPromise);
promisesMap.has(referencedROINumber)
? promisesMap.get(referencedROINumber).push(bulkDataPromise)
: promisesMap.set(referencedROINumber, [bulkDataPromise]);
} else {
return Promise.reject(`Invalid ContourData: ${contourData}`);
}
}
}
const flattenedPromises = promises.flat();
const resolvedPromises = await Promise.allSettled(flattenedPromises);
// Modify contourData and replace it in its corresponding ROIContourSequence's Contour's contourData
let index = 0;
instance.ROIContourSequence.forEach((ROIContour, roiIndex) => {
ROIContour.ContourSequence.forEach((Contour, contourIndex) => {
const promise = resolvedPromises[index++];
const resolvedPromisesMap = new Map();
for (const [key, promiseArray] of promisesMap.entries()) {
resolvedPromisesMap.set(key, await Promise.allSettled(promiseArray));
}
if (promise.status === 'fulfilled') {
const uint8Array = new Uint8Array(promise.value);
const textDecoder = new TextDecoder();
const dataUint8Array = textDecoder.decode(uint8Array);
if (
typeof dataUint8Array === 'string' &&
dataUint8Array.includes('\\')
) {
const numSlashes = (dataUint8Array.match(/\\/g) || []).length;
let startIndex = 0;
let endIndex = dataUint8Array.indexOf('\\', startIndex);
let numbersParsed = 0;
const ContourData = [];
instance.ROIContourSequence.forEach(ROIContour => {
try {
const referencedROINumber = ROIContour.ReferencedROINumber;
const resolvedPromises = resolvedPromisesMap.get(referencedROINumber);
while (numbersParsed !== numSlashes + 1) {
const str = dataUint8Array.substring(startIndex, endIndex);
let value = parseFloat(str);
ContourData.push(value);
startIndex = endIndex + 1;
endIndex = dataUint8Array.indexOf('\\', startIndex);
endIndex === -1 ? (endIndex = dataUint8Array.length) : endIndex;
numbersParsed++;
if (ROIContour.ContourSequence) {
ROIContour.ContourSequence.forEach((Contour, index) => {
const promise = resolvedPromises[index];
if (promise.status === 'fulfilled') {
if (
Array.isArray(promise.value) &&
promise.value.every(Number.isFinite)
) {
// If promise.value is already an array of numbers, use it directly
Contour.ContourData = promise.value;
} else {
// If the resolved promise value is a byte array (Blob), it needs to be decoded
const uint8Array = new Uint8Array(promise.value);
const textDecoder = new TextDecoder();
const dataUint8Array = textDecoder.decode(uint8Array);
if (
typeof dataUint8Array === 'string' &&
dataUint8Array.includes('\\')
) {
Contour.ContourData = dataUint8Array
.split('\\')
.map(parseFloat);
} else {
Contour.ContourData = [];
}
}
} else {
console.error(promise.reason);
}
Contour.ContourData = ContourData;
} else {
Contour.ContourData = [];
}
} else {
console.error(promise.reason);
});
}
});
} catch (error) {
console.error(error);
}
});
}
@ -104,7 +117,7 @@ export default async function loadRTStruct(
'@ohif/extension-cornerstone.utilityModule.common'
);
const dataSource = extensionManager.getActiveDataSource()[0];
const { useBulkDataURI } = dataSource.getConfig?.() || {};
const { bulkDataURI } = dataSource.getConfig?.() || {};
const { dicomLoaderService } = utilityModule.exports;
const imageIdSopInstanceUidPairs = _getImageIdSopInstanceUidPairsForDisplaySet(
@ -116,7 +129,7 @@ export default async function loadRTStruct(
rtStructDisplaySet.isLoaded = true;
let instance = rtStructDisplaySet.instance;
if (!useBulkDataURI) {
if (!bulkDataURI || !bulkDataURI.enabled) {
const segArrayBuffer = await dicomLoaderService.findDicomDataPromise(
rtStructDisplaySet,
null,

View File

@ -45,8 +45,8 @@
"dependencies": {
"@babel/runtime": "^7.20.13",
"classnames": "^2.3.2",
"@cornerstonejs/adapters": "^0.6.0",
"@cornerstonejs/core": "^0.47.1",
"@cornerstonejs/tools": "^0.67.4"
"@cornerstonejs/adapters": "^1.1.0",
"@cornerstonejs/core": "^1.1.0",
"@cornerstonejs/tools": "^1.1.0"
}
}

View File

@ -34,7 +34,7 @@
"peerDependencies": {
"@ohif/core": "^3.0.0",
"@ohif/ui": "^2.0.0",
"@cornerstonejs/dicom-image-loader": "^0.6.6",
"@cornerstonejs/dicom-image-loader": "^0.6.8",
"@cornerstonejs/codec-charls": "^1.2.3",
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.2",
@ -52,10 +52,10 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^0.6.0",
"@cornerstonejs/core": "^0.47.1",
"@cornerstonejs/streaming-image-volume-loader": "^0.20.4",
"@cornerstonejs/tools": "^0.67.4",
"@cornerstonejs/adapters": "^1.1.0",
"@cornerstonejs/core": "^1.1.0",
"@cornerstonejs/streaming-image-volume-loader": "^1.1.0",
"@cornerstonejs/tools": "^1.1.0",
"@kitware/vtk.js": "27.3.1",
"html2canvas": "^1.4.1",
"lodash.debounce": "4.0.8",

View File

@ -335,13 +335,13 @@ const OHIFCornerstoneViewport = React.memo(props => {
cleanUpServices();
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
cornerstoneViewportService.disableElement(viewportIndex);
if (onElementDisabled) {
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
onElementDisabled(viewportInfo);
}

View File

@ -61,8 +61,8 @@ export default function initWADOImageLoader(
const xhrRequestHeaders = {}
if (headers && headers.Authorization) {
xhrRequestHeaders.Authorization = headers.Authorization;
if (headers) {
Object.assign(xhrRequestHeaders, headers);
}
return xhrRequestHeaders;

View File

@ -38,6 +38,7 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-i18next": "^12.2.2",
"react-window": "^1.8.9",
"webpack": "^5.50.0",
"webpack-merge": "^5.7.3"
},

View File

@ -1,11 +1,11 @@
import ContextMenuController from './ContextMenuController';
import * as ContextMenuItemsBuilder from './ContextMenuItemsBuilder';
import defaultContextMenu from './defaultContextMenu';
import * as CustomizeableContextMenuTypes from './types';
import * as CustomizableContextMenuTypes from './types';
export {
ContextMenuController,
CustomizeableContextMenuTypes,
CustomizableContextMenuTypes,
ContextMenuItemsBuilder,
defaultContextMenu,
};

View File

@ -1,27 +1,41 @@
import dcmjs from 'dcmjs';
import moment from 'moment';
import React, { useState, useMemo } from 'react';
import React, { useState, useMemo, useEffect, useRef } from 'react';
import { classes } from '@ohif/core';
import { Icon, InputRange, Select, Typography } from '@ohif/ui';
import debounce from 'lodash.debounce';
import classNames from 'classnames';
import DicomTagTable from './DicomTagTable';
import './DicomTagBrowser.css';
import { InputRange, Select, Typography } from '@ohif/ui';
const { ImageSet } = classes;
const { DicomMetaDictionary } = dcmjs.data;
const { nameMap } = DicomMetaDictionary;
const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
// The column indices that are to be excluded during a filter of the table.
// At present the column indices are:
// 0: DICOM tag
// 1: VR
// 2: Keyword
// 3: Value
const excludedColumnIndicesForFilter: Set<number> = new Set([1]);
const [
selectedDisplaySetInstanceUID,
setSelectedDisplaySetInstanceUID,
] = useState(displaySetInstanceUID);
const [instanceNumber, setInstanceNumber] = useState(1);
const [filterValue, setFilterValue] = useState('');
const onSelectChange = value => {
setSelectedDisplaySetInstanceUID(value.value);
setInstanceNumber(1);
};
const searchInputRef = useRef(null);
const activeDisplaySet = displaySets.find(
ds => ds.displaySetInstanceUID === selectedDisplaySetInstanceUID
);
@ -54,64 +68,130 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
});
}, [displaySets]);
const rows = useMemo(() => {
let metadata;
if (isImageStack) {
metadata = activeDisplaySet.images[instanceNumber - 1];
} else {
metadata = activeDisplaySet.instance || activeDisplaySet;
}
const tags = getSortedTags(metadata);
return getFormattedRowsFromTags(tags, metadata);
}, [instanceNumber, selectedDisplaySetInstanceUID]);
const filteredRows = useMemo(() => {
if (!filterValue) {
return rows;
}
const filterValueLowerCase = filterValue.toLowerCase();
return rows.filter(row => {
return row.reduce((keepRow, col, colIndex) => {
if (keepRow) {
// We are already keeping the row, why do more work so return now.
return keepRow;
}
if (excludedColumnIndicesForFilter.has(colIndex)) {
return keepRow;
}
return keepRow || col.toLowerCase().includes(filterValueLowerCase);
}, false);
});
}, [rows, filterValue]);
const debouncedSetFilterValue = useMemo(() => {
return debounce(setFilterValue, 200);
}, []);
useEffect(() => {
return () => {
debouncedSetFilterValue?.cancel();
};
}, []);
return (
<div className="dicom-tag-browser-content">
<div className="flex flex-row items-center mb-2">
<Typography variant="subtitle" className="w-1/2 mr-8">
Series
</Typography>
{showInstanceList && (
<Typography variant="subtitle" className="w-1/2">
Instance Number
<div className="flex flex-row mb-6 items-center pl-1">
<div className="flex flex-row items-center w-1/2">
<Typography variant="subtitle" className="mr-4">
Series
</Typography>
)}
</div>
<div className="flex flex-row items-center mb-6">
<div className="w-1/2 mr-8">
<Select
isClearable={false}
onChange={onSelectChange}
options={displaySetList}
value={displaySetList.find(
ds => ds.value === selectedDisplaySetInstanceUID
)}
className="text-white"
/>
</div>
{showInstanceList ? (
<div className="w-1/2">
<InputRange
value={instanceNumber}
key={selectedDisplaySetInstanceUID}
onChange={value => {
setInstanceNumber(parseInt(value));
}}
minValue={1}
maxValue={activeDisplaySet.images.length}
step={1}
<div className="grow mr-8">
<Select
id="display-set-selector"
isClearable={false}
onChange={onSelectChange}
options={displaySetList}
value={displaySetList.find(
ds => ds.value === selectedDisplaySetInstanceUID
)}
className="text-white"
/>
</div>
) : null}
</div>
<div className="flex flex-row items-center w-1/2">
{showInstanceList && (
<Typography variant="subtitle" className="mr-4">
Instance Number
</Typography>
)}
{showInstanceList && (
<div className="grow">
<InputRange
value={instanceNumber}
key={selectedDisplaySetInstanceUID}
onChange={value => {
setInstanceNumber(parseInt(value));
}}
minValue={1}
maxValue={activeDisplaySet.images.length}
step={1}
inputClassName="w-full"
labelPosition="left"
trackColor={'#3a3f99'}
/>
</div>
)}
</div>
</div>
<DicomTagTable
rows={getFormattedRowsFromTags(activeDisplaySet, instanceNumber)}
></DicomTagTable>
<div className="w-full h-1 bg-black"></div>
<div className="flex flex-row my-3 w-1/2">
{/* TODO - refactor the following into its own reusable component */}
<label className="relative block w-full mr-8">
<span className="absolute inset-y-0 left-0 flex items-center pl-2">
<Icon name="icon-search"></Icon>
</span>
<input
ref={searchInputRef}
type="text"
className="block bg-black w-full shadow transition duration-300 appearance-none border border-inputfield-main focus:border-inputfield-focus focus:outline-none disabled:border-inputfield-disabled rounded w-full py-2 px-9 text-base leading-tight placeholder:text-inputfield-placeholder"
placeholder="Search metadata..."
onChange={event => debouncedSetFilterValue(event.target.value)}
autoComplete="off"
></input>
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
<Icon
name="icon-clear-field"
className={classNames(
'cursor-pointer',
filterValue ? '' : 'hidden'
)}
onClick={() => {
searchInputRef.current.value = '';
debouncedSetFilterValue('');
}}
></Icon>
</span>
</label>
</div>
<DicomTagTable rows={filteredRows} />
</div>
);
};
function getFormattedRowsFromTags(displaySet, instanceNumber) {
const isImageStack = _isImageStack(displaySet);
let metadata;
if (isImageStack) {
metadata = displaySet.images[instanceNumber - 1];
} else {
metadata = displaySet;
}
const tags = getSortedTags(metadata);
function getFormattedRowsFromTags(tags, metadata) {
const rows = [];
tags.forEach(tagInfo => {
@ -126,7 +206,7 @@ function getFormattedRowsFromTags(displaySet, instanceNumber) {
const { values } = tagInfo;
values.forEach((item, index) => {
const formatedRowsFromTags = getFormattedRowsFromTags(item);
const formatedRowsFromTags = getFormattedRowsFromTags(item, metadata);
rows.push([
`${item[0].tagIndent}(FFFE,E000)`,
@ -140,34 +220,21 @@ function getFormattedRowsFromTags(displaySet, instanceNumber) {
} else {
if (tagInfo.vr === 'xs') {
try {
/* const dataset = metadataProvider.getStudyDataset(
meta.StudyInstanceUID
);*/
// console.log(dataset);
// const tag = dcmjs.data.Tag.fromPString(tagInfo.tag).toCleanString();
// const originalTagInfo = dataset[tag];
// tagInfo.vr = originalTagInfo.vr;
const tag = dcmjs.data.Tag.fromPString(tagInfo.tag).toCleanString();
const originalTagInfo = metadata[tag];
tagInfo.vr = originalTagInfo.vr;
} catch (error) {
console.error(
`Failed to parse value representation for tag '${tagInfo.keyword}'`
);
}
}
if (tagInfo.vr === 'PN') {
rows.push([
`${tagInfo.tagIndent}${tagInfo.tag}`,
tagInfo.vr,
tagInfo.keyword,
tagInfo.value,
]);
} else {
rows.push([
`${tagInfo.tagIndent}${tagInfo.tag}`,
tagInfo.vr,
tagInfo.keyword,
tagInfo.value,
]);
}
rows.push([
`${tagInfo.tagIndent}${tagInfo.tag}`,
tagInfo.vr,
tagInfo.keyword,
tagInfo.value,
]);
}
});

View File

@ -1,97 +1,224 @@
import React from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { VariableSizeList as List } from 'react-window';
import classNames from 'classnames';
import debounce from 'lodash.debounce';
function ColumnHeaders() {
const lineHeightPx = 20;
const lineHeightClassName = `leading-[${lineHeightPx}px]`;
const rowVerticalPaddingPx = 10;
const rowBottomBorderPx = 1;
const rowVerticalPaddingStyle = { padding: `${rowVerticalPaddingPx}px 0` };
const rowStyle = {
borderBottomWidth: `${rowBottomBorderPx}px`,
...rowVerticalPaddingStyle,
};
function ColumnHeaders({ tagRef, vrRef, keywordRef, valueRef }) {
return (
<div className="m-auto flex flex-col py-2 bg-secondary-light">
<div className="flex flex-row w-full">
<div className="px-3 w-5/24">
<label className="flex flex-col flex-1 text-white text-lg pl-1 select-none">
<span className="flex flex-row items-center focus:outline-none">
Tag
</span>
</label>
</div>
<div className="px-3 w-4/24">
<label className="flex flex-col flex-1 text-white text-lg pl-1 select-none">
<span className="flex flex-row items-center focus:outline-none">
VR
</span>
</label>
</div>
<div className="px-3 w-6/24">
<label className="flex flex-col flex-1 text-white text-lg pl-1 select-none">
<span className="flex flex-row items-center focus:outline-none">
Keyword
</span>
</label>
</div>
<div className="px-3 w-5/24">
<label className="flex flex-col flex-1 text-white text-lg pl-1 select-none">
<span className="flex flex-row items-center focus:outline-none">
Value
</span>
</label>
</div>
<div
className={classNames(
'flex flex-row w-full bg-secondary-dark ohif-scrollbar overflow-y-scroll'
)}
style={rowVerticalPaddingStyle}
>
<div className="px-3 w-4/24">
<label
ref={tagRef}
className="flex flex-col flex-1 text-white text-lg pl-1 select-none"
>
<span className="flex flex-row items-center focus:outline-none">
Tag
</span>
</label>
</div>
<div className="px-3 w-2/24">
<label
ref={vrRef}
className="flex flex-col flex-1 text-white text-lg pl-1 select-none"
>
<span className="flex flex-row items-center focus:outline-none">
VR
</span>
</label>
</div>
<div className="px-3 w-6/24">
<label
ref={keywordRef}
className="flex flex-col flex-1 text-white text-lg pl-1 select-none"
>
<span className="flex flex-row items-center focus:outline-none">
Keyword
</span>
</label>
</div>
<div className="px-3 w-5/24 grow">
<label
ref={valueRef}
className="flex flex-col flex-1 text-white text-lg pl-1 select-none"
>
<span className="flex flex-row items-center focus:outline-none">
Value
</span>
</label>
</div>
</div>
);
}
function DicomTagTable({ rows }) {
const listRef = useRef();
const canvasRef = useRef();
const [tagHeaderElem, setTagHeaderElem] = useState(null);
const [vrHeaderElem, setVrHeaderElem] = useState(null);
const [keywordHeaderElem, setKeywordHeaderElem] = useState(null);
const [valueHeaderElem, setValueHeaderElem] = useState(null);
// Here the refs are inturn stored in state to trigger a render of the table.
// This virtualized table does NOT render until the header is rendered because the header column widths are used to determine the row heights in the table.
// Therefore whenever the refs change (in particular the first time the refs are set), we want to trigger a render of the table.
const tagRef = elem => {
if (elem) {
setTagHeaderElem(elem);
}
};
const vrRef = elem => {
if (elem) {
setVrHeaderElem(elem);
}
};
const keywordRef = elem => {
if (elem) {
setKeywordHeaderElem(elem);
}
};
const valueRef = elem => {
if (elem) {
setValueHeaderElem(elem);
}
};
/**
* When new rows are set, scroll to the top and reset the virtualization.
*/
useEffect(() => {
if (!listRef?.current) {
return;
}
listRef.current.scrollTo(0);
listRef.current.resetAfterIndex(0);
}, [rows]);
/**
* When the browser window resizes, update the row virtualization (i.e. row heights)
*/
useEffect(() => {
const debouncedResize = debounce(
() => listRef.current.resetAfterIndex(0),
100
);
window.addEventListener('resize', debouncedResize);
return () => {
debouncedResize.cancel();
window.removeEventListener('resize', debouncedResize);
};
}, []);
const Row = useCallback(
({ index, style }) => {
const row = rows[index];
return (
<div
style={{ ...style, ...rowStyle }}
className={classNames(
'hover:bg-secondary-main transition duration-300 bg-black flex flex-row w-full border-secondary-light items-center text-base break-all',
lineHeightClassName
)}
key={`DICOMTagRow-${index}`}
>
<div className="px-3 w-4/24">{row[0]}</div>
<div className="px-3 w-2/24">{row[1]}</div>
<div className="px-3 w-6/24">{row[2]}</div>
<div className="px-3 w-5/24 grow">{row[3]}</div>
</div>
);
},
[rows]
);
/**
* Whenever any one of the column headers is set, then the header is rendered.
* Here we chose the tag header.
*/
const isHeaderRendered = useCallback(() => tagHeaderElem !== null, [
tagHeaderElem,
]);
/**
* Get the item/row size. We use the header column widths to calculate the various row heights.
* @param index the row index
* @returns the row height
*/
const getItemSize = useCallback(
index => {
const headerWidths = [
tagHeaderElem.offsetWidth,
vrHeaderElem.offsetWidth,
keywordHeaderElem.offsetWidth,
valueHeaderElem.offsetWidth,
];
const context = canvasRef.current.getContext('2d');
context.font = getComputedStyle(canvasRef.current).font;
return rows[index]
.map((colText, index) => {
const colOneLineWidth = context.measureText(colText).width;
const numLines = Math.ceil(colOneLineWidth / headerWidths[index]);
return (
numLines * lineHeightPx +
2 * rowVerticalPaddingPx +
rowBottomBorderPx
);
})
.reduce((maxHeight, colHeight) => Math.max(maxHeight, colHeight));
},
[rows, keywordHeaderElem, tagHeaderElem, valueHeaderElem, vrHeaderElem]
);
return (
<div>
{ColumnHeaders()}
<canvas
style={{ visibility: 'hidden', position: 'absolute' }}
className="text-base"
ref={canvasRef}
/>
<ColumnHeaders
tagRef={tagRef}
vrRef={vrRef}
keywordRef={keywordRef}
valueRef={valueRef}
/>
<div
className="m-auto relative border-2 border-secondary-light overflow-hidden ohif-scrollbar"
className="m-auto relative border-2 border-black bg-black"
style={{ height: '32rem' }}
>
<table className="w-full text-white">
<tbody>
{rows.map((row, index) => {
const className = row.className ? row.className : null;
return (
<tr
className="hover:bg-secondary-main transition duration-300 bg-primary-dark"
key={`DICOMTagRow-${index}`}
>
<td
style={{ maxWidth: '0px' }}
className="px-4 py-2 text-base break-all border-b border-secondary-light w-5/24"
>
<div className="flex">
<div className="inline-flex max-w-full">{row[0]}</div>
</div>
</td>
<td
style={{ maxWidth: '0px' }}
className="px-4 py-2 text-base break-all border-b border-secondary-light w-4/24"
>
<div className="flex">
<div className="inline-flex max-w-full">{row[1]}</div>
</div>
</td>
<td
style={{ maxWidth: '0px' }}
className="px-4 py-2 text-base break-all border-b border-secondary-light w-6/24"
>
<div className="flex">
<div className="inline-flex max-w-full">{row[2]}</div>
</div>
</td>
<td
style={{ maxWidth: '0px' }}
className="px-4 py-2 text-base break-all border-b border-secondary-light w-14/24"
>
<div className="flex">
<div className="inline-flex max-w-full">{row[3]}</div>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
{isHeaderRendered() && (
<List
ref={listRef}
height={500}
itemCount={rows.length}
itemSize={getItemSize}
width={'100%'}
className="ohif-scrollbar"
>
{Row}
</List>
)}
</div>
</div>
);

View File

@ -24,6 +24,7 @@ import {
} from './retrieveStudyMetadata.js';
import StaticWadoClient from './utils/StaticWadoClient';
import getDirectURL from '../utils/getDirectURL';
import { fixBulkDataURI } from './utils/fixBulkDataURI';
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
@ -432,13 +433,23 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
*/
const addRetrieveBulkData = instance => {
const naturalized = naturalizeDataset(instance);
// if we konw the server doesn't use bulkDataURI, then don't
if (!dicomWebConfig.bulkDataURI?.enabled) {
return naturalized;
}
Object.keys(naturalized).forEach(key => {
const value = naturalized[key];
// The value.Value will be set with the bulkdata read value
// in which case it isn't necessary to re-read this.
if (value && value.BulkDataURI && !value.Value) {
// Provide a method to fetch bulkdata
value.retrieveBulkData = () => {
// handle the scenarios where bulkDataURI is relative path
fixBulkDataURI(value, naturalized, dicomWebConfig);
const options = {
// The bulkdata fetches work with either multipart or
// singlepart, so set multipart to false to let the server

View File

@ -0,0 +1,56 @@
/**
* Modifies a bulkDataURI to ensure it is absolute based on the DICOMWeb configuration and
* instance data. The modification is in-place.
*
* If the bulkDataURI is relative to the series or study (according to the DICOM standard),
* it is made absolute by prepending the relevant paths.
*
* In scenarios where the bulkDataURI is a server-relative path (starting with '/'), the function
* handles two cases:
*
* 1. If the wado root is absolute (starts with 'http'), it prepends the wado root to the bulkDataURI.
* 2. If the wado root is relative, no changes are needed as the bulkDataURI is already correctly relative to the server root.
*
* @param value - The object containing BulkDataURI to be fixed.
* @param instance - The object (DICOM instance data) containing StudyInstanceUID and SeriesInstanceUID.
* @param dicomWebConfig - The DICOMWeb configuration object, containing wadoRoot and potentially bulkDataURI.relativeResolution.
* @returns The function modifies `value` in-place, it does not return a value.
*/
function fixBulkDataURI(value, instance, dicomWebConfig) {
// in case of the relative path, make it absolute. The current DICOM standard says
// the bulkdataURI is relative to the series. However, there are situations where
// it can be relative to the study too
if (
!value.BulkDataURI.startsWith('http') &&
!value.BulkDataURI.startsWith('/')
) {
if (dicomWebConfig.bulkDataURI?.relativeResolution === 'studies') {
value.BulkDataURI = `${dicomWebConfig.wadoRoot}/studies/${instance.StudyInstanceUID}/${value.BulkDataURI}`;
} else if (
dicomWebConfig.bulkDataURI?.relativeResolution === 'series' ||
!dicomWebConfig.bulkDataURI?.relativeResolution
) {
value.BulkDataURI = `${dicomWebConfig.wadoRoot}/studies/${instance.StudyInstanceUID}/series/${instance.SeriesInstanceUID}/${value.BulkDataURI}`;
}
return;
}
// in case it is relative path but starts at the server (e.g., /bulk/1e, note the missing http
// in the beginning and the first character is /) There are two scenarios, whether the wado root
// is absolute or relative. In case of absolute, we need to prepend the wado root to the bulkdata
// uri (e.g., bulkData: /bulk/1e, wado root: http://myserver.com/dicomweb, output: http://myserver.com/bulk/1e)
// and in case of relative wado root, we need to prepend the bulkdata uri to the wado root (e.g,. bulkData: /bulk/1e
// wado root: /dicomweb, output: /bulk/1e)
if (value.BulkDataURI[0] === '/') {
if (dicomWebConfig.wadoRoot.startsWith('http')) {
// Absolute wado root
const url = new URL(dicomWebConfig.wadoRoot);
value.BulkDataURI = `${url.origin}${value.BulkDataURI}`;
} else {
// Relative wado root, we don't need to do anything, bulkdata uri is already correct
}
}
}
export { fixBulkDataURI };

View File

@ -0,0 +1,3 @@
import { fixBulkDataURI } from './fixBulkDataURI';
export { fixBulkDataURI };

View File

@ -3,14 +3,14 @@ import { ServicesManager, utils, Types } from '@ohif/core';
import {
ContextMenuController,
defaultContextMenu,
} from './CustomizeableContextMenu';
} from './CustomizableContextMenu';
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
import reuseCachedLayouts from './utils/reuseCachedLayouts';
import findViewportsByPosition, {
findOrCreateViewport as layoutFindOrCreate,
} from './findViewportsByPosition';
import { ContextMenuProps } from './CustomizeableContextMenu/types';
import { ContextMenuProps } from './CustomizableContextMenu/types';
import { NavigateHistory } from './types/commandModuleTypes';
import { history } from '@ohif/viewer';
@ -23,6 +23,11 @@ export type HangingProtocolParams = {
stageId?: string;
};
export type UpdateViewportDisplaySetParams = {
direction: number;
excludeNonImageModalities?: boolean;
};
/**
* Determine if a command is a hanging protocol one.
* For now, just use the two hanging protocol commands that are in this
@ -541,6 +546,130 @@ const commandsModule = ({
overlays.item(i).classList.toggle('hidden');
}
},
scrollActiveThumbnailIntoView: () => {
const { activeViewportIndex, viewports } = viewportGridService.getState();
if (
!viewports ||
activeViewportIndex < 0 ||
activeViewportIndex > viewports.length - 1
) {
return;
}
const activeViewport = viewports[activeViewportIndex];
const activeDisplaySetInstanceUID =
activeViewport.displaySetInstanceUIDs[0];
const thumbnailList = document.querySelector('#ohif-thumbnail-list');
if (!thumbnailList) {
return;
}
const thumbnailListBounds = thumbnailList.getBoundingClientRect();
const thumbnail = document.querySelector(
`#thumbnail-${activeDisplaySetInstanceUID}`
);
if (!thumbnail) {
return;
}
const thumbnailBounds = thumbnail.getBoundingClientRect();
// This only handles a vertical thumbnail list.
if (
thumbnailBounds.top >= thumbnailListBounds.top &&
thumbnailBounds.top <= thumbnailListBounds.bottom
) {
return;
}
thumbnail.scrollIntoView({ behavior: 'smooth' });
},
updateViewportDisplaySet: ({
direction,
excludeNonImageModalities,
}: UpdateViewportDisplaySetParams) => {
const nonImageModalities = [
'SR',
'SEG',
'SM',
'RTSTRUCT',
'RTPLAN',
'RTDOSE',
];
// Sort the display sets as per the hanging protocol service viewport/display set scoring system.
// The thumbnail list uses the same sorting.
const dsSortFn = hangingProtocolService.getDisplaySetSortFunction();
const currentDisplaySets = [...displaySetService.activeDisplaySets];
currentDisplaySets.sort(dsSortFn);
const { activeViewportIndex, viewports } = viewportGridService.getState();
const { displaySetInstanceUIDs } = viewports[activeViewportIndex];
const activeDisplaySetIndex = currentDisplaySets.findIndex(displaySet =>
displaySetInstanceUIDs.includes(displaySet.displaySetInstanceUID)
);
let displaySetIndexToShow: number;
for (
displaySetIndexToShow = activeDisplaySetIndex + direction;
displaySetIndexToShow > -1 &&
displaySetIndexToShow < currentDisplaySets.length;
displaySetIndexToShow += direction
) {
if (
!excludeNonImageModalities ||
!nonImageModalities.includes(
currentDisplaySets[displaySetIndexToShow].Modality
)
) {
break;
}
}
if (
displaySetIndexToShow < 0 ||
displaySetIndexToShow >= currentDisplaySets.length
) {
return;
}
const { displaySetInstanceUID } = currentDisplaySets[
displaySetIndexToShow
];
let updatedViewports = [];
try {
updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
activeViewportIndex,
displaySetInstanceUID
);
} catch (error) {
console.warn(error);
uiNotificationService.show({
title: 'Navigate Viewport Display Set',
message:
'The requested display sets could not be added to the viewport due to a mismatch in the Hanging Protocol rules.',
type: 'info',
duration: 3000,
});
}
viewportGridService.setDisplaySetsForViewports(updatedViewports);
setTimeout(() => actions.scrollActiveThumbnailIntoView(), 0);
},
};
const definitions = {
@ -598,6 +727,11 @@ const commandsModule = ({
openDICOMTagViewer: {
commandFn: actions.openDICOMTagViewer,
},
updateViewportDisplaySet: {
commandFn: actions.updateViewportDisplaySet,
storeContexts: [],
options: {},
},
};
return {

View File

@ -13,8 +13,9 @@ import { id } from './id.js';
import preRegistration from './init';
import {
ContextMenuController,
CustomizeableContextMenuTypes,
} from './CustomizeableContextMenu';
CustomizableContextMenuTypes,
} from './CustomizableContextMenu';
import * as dicomWebUtils from './DicomWebDataSource/utils';
const defaultExtension: Types.Extensions.Extension = {
/**
@ -47,6 +48,7 @@ export default defaultExtension;
export {
ContextMenuController,
CustomizeableContextMenuTypes,
CustomizableContextMenuTypes,
getStudiesForPatientByMRN,
dicomWebUtils,
};

View File

@ -1,10 +1,4 @@
import {
DicomMetadataStore,
IWebApiDataSource,
utils,
errorHandler,
classes,
} from '@ohif/core';
import { utils } from '@ohif/core';
/**
* Generates a URL that can be used for direct retrieve of the bulkdata
@ -57,31 +51,19 @@ const getDirectURL = (config, params) => {
const BulkDataURI =
(value && value.BulkDataURI) ||
`series/${SeriesInstanceUID}/instances/${SOPInstanceUID}${defaultPath}`;
const hasQuery = BulkDataURI.indexOf('?') != -1;
const hasAccept = BulkDataURI.indexOf('accept=') != -1;
const hasQuery = BulkDataURI.indexOf('?') !== -1;
const hasAccept = BulkDataURI.indexOf('accept=') !== -1;
const acceptUri =
BulkDataURI +
(hasAccept ? '' : (hasQuery ? '&' : '?') + `accept=${defaultType}`);
if (BulkDataURI.indexOf('http') === 0) {
if (tag === 'PixelData' || tag === 'EncapsulatedDocument') {
return `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/rendered`;
} else {
return acceptUri;
}
if (tag === 'PixelData' || tag === 'EncapsulatedDocument') {
return `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/rendered`;
}
if (BulkDataURI.indexOf('/') === 0) {
return wadoRoot + acceptUri;
}
if (BulkDataURI.indexOf('series/') == 0) {
return `${wadoRoot}/studies/${StudyInstanceUID}/${acceptUri}`;
}
if (BulkDataURI.indexOf('instances/') === 0) {
return `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/${acceptUri}`;
}
if (BulkDataURI.indexOf('bulkdata/') === 0) {
return `${wadoRoot}/studies/${StudyInstanceUID}/${acceptUri}`;
}
throw new Error('BulkDataURI in unknown format:' + BulkDataURI);
// The DICOMweb standard states that the default is multipart related, and then
// separately states that the accept parameter is the URL parameter equivalent of the accept header.
return acceptUri;
};
export default getDirectURL;

View File

@ -1,3 +1,10 @@
.DicomMicroscopyViewer {
--ol-partial-background-color: rgba(127, 127, 127, 0.7);
--ol-foreground-color: #000000;
--ol-subtle-foreground-color: #000;
--ol-subtle-background-color: rgba(78, 78, 78, 0.5);
}
.DicomMicroscopyViewer .ol-box {
box-sizing: border-box;
border-radius: 2px;
@ -332,7 +339,7 @@
}
.DicomMicroscopyViewer .ol-overviewmap-box {
border: 1.5px dotted var(--ol-subtle-foreground-color);
border: 0.5px dotted var(--ol-subtle-foreground-color);
}
.DicomMicroscopyViewer .ol-overviewmap .ol-overviewmap-box:hover {

View File

@ -2,6 +2,7 @@ import React, { Component } from 'react';
import ReactResizeDetector from 'react-resize-detector';
import PropTypes from 'prop-types';
import debounce from 'lodash.debounce';
import { LoadingIndicatorProgress } from '@ohif/ui';
import './DicomMicroscopyViewport.css';
import ViewportOverlay from './components/ViewportOverlay';
@ -10,9 +11,20 @@ import dcmjs from 'dcmjs';
import cleanDenaturalizedDataset from './utils/cleanDenaturalizedDataset';
import MicroscopyService from './services/MicroscopyService';
function transformImageTypeUnnaturalized(entry) {
if (entry.vr === 'CS') {
return {
vr: 'US',
Value: entry.Value[0].split('\\'),
};
}
return entry;
}
class DicomMicroscopyViewport extends Component {
state = {
error: null as any,
isLoaded: false,
};
microscopyService: MicroscopyService;
@ -132,6 +144,10 @@ class DicomMicroscopyViewport extends Component {
// );
// m['00200052'].Value[0] = volumeImages[0].FrameOfReferenceUID;
// }
// NOTE: depending on different data source, image.ImageType sometimes
// is a string, not a string array.
// m['00080008'] = transformImageTypeUnnaturalized(m['00080008']);
// const image = new metadataUtils.VLWholeSlideMicroscopyImage({
// metadata: m,
// });
@ -143,8 +159,20 @@ class DicomMicroscopyViewport extends Component {
// }
metadata.forEach(m => {
// NOTE: depending on different data source, image.ImageType sometimes
// is a string, not a string array.
m.ImageType =
typeof m.ImageType === 'string'
? m.ImageType.split('\\')
: m.ImageType;
const inst = cleanDenaturalizedDataset(
dcmjs.data.DicomMetaDictionary.denaturalizeDataset(m)
dcmjs.data.DicomMetaDictionary.denaturalizeDataset(m),
{
StudyInstanceUID: m.StudyInstanceUID,
SeriesInstanceUID: m.SeriesInstanceUID,
dataSourceConfig: this.props.dataSource.getConfig(),
}
);
if (!inst['00480105']) {
// Optical Path Sequence, no OpticalPathIdentifier?
@ -164,6 +192,7 @@ class DicomMicroscopyViewport extends Component {
const image = new metadataUtils.VLWholeSlideMicroscopyImage({
metadata: inst,
});
const imageFlavor = image.ImageType[2];
if (imageFlavor === 'VOLUME' || imageFlavor === 'THUMBNAIL') {
volumeImages.push(image);
@ -175,7 +204,7 @@ class DicomMicroscopyViewport extends Component {
client,
metadata: volumeImages,
retrieveRendered: false,
controls: ['overview', 'position', 'zoom'],
controls: ['overview', 'position'],
};
this.viewer = new microscopyViewer(options);
@ -230,7 +259,11 @@ class DicomMicroscopyViewport extends Component {
componentDidMount() {
const { displaySets, viewportIndex } = this.props;
const displaySet = displaySets[viewportIndex];
this.installOpenLayersRenderer(this.container.current, displaySet);
this.installOpenLayersRenderer(this.container.current, displaySet).then(
() => {
this.setState({ isLoaded: true });
}
);
}
componentDidUpdate(
@ -309,6 +342,9 @@ class DicomMicroscopyViewport extends Component {
) : (
<div style={style} ref={this.container} />
)}
{this.state.isLoaded ? null : (
<LoadingIndicatorProgress className={'w-full h-full bg-black'} />
)}
</div>
);
}

View File

@ -1,6 +1,5 @@
import React from 'react';
import classnames from 'classnames';
import ConfigPoint from 'config-point';
import listComponentGenerator from './listComponentGenerator';
import './ViewportOverlay.css';
@ -11,7 +10,7 @@ import {
formatPN,
} from './utils';
interface OverylayItem {
interface OverlayItem {
id: string;
title: string;
value?: (props: any) => string;
@ -28,17 +27,17 @@ interface OverylayItem {
* @returns
*/
export const generateFromConfig = ({
topLeft,
topRight,
bottomLeft,
bottomRight,
itemGenerator,
topLeft = [],
topRight = [],
bottomLeft = [],
bottomRight = [],
itemGenerator = () => {},
}: {
topLeft: OverylayItem[];
topRight: OverylayItem[];
bottomLeft: OverylayItem[];
bottomRight: OverylayItem[];
itemGenerator: (props: any) => any;
topLeft?: OverlayItem[];
topRight?: OverlayItem[];
bottomLeft?: OverlayItem[];
bottomRight?: OverlayItem[];
itemGenerator?: (props: any) => any;
}) => {
return (props: any) => {
const topLeftClass = 'top-viewport left-viewport text-primary-light';
@ -127,29 +126,4 @@ const itemGenerator = (props: any) => {
);
};
const { MicroscopyViewportOverlay } = ConfigPoint.register({
MicroscopyViewportOverlay: {
configBase: {
topLeft: [
// {
// id: 'sm-overlay-patient-name',
// title: 'PatientName',
// condition: ({ instance }) =>
// instance && instance.PatientName && instance.PatientName.Alphabetic,
// value: ({ instance }) =>
// instance.PatientName && instance.PatientName.Alphabetic,
// } as OverylayItem,
],
topRight: [] as OverylayItem[],
bottomLeft: [] as OverylayItem[],
bottomRight: [] as OverylayItem[],
itemGenerator,
generateFromConfig,
},
...(window.config?.MicroscopyViewportOverlay || {}),
},
});
export default MicroscopyViewportOverlay.generateFromConfig(
MicroscopyViewportOverlay
);
export default generateFromConfig({});

View File

@ -1,3 +1,5 @@
import { dicomWebUtils } from '@ohif/extension-default';
function isPrimitive(v: any) {
return !(typeof v == 'object' || Array.isArray(v));
}
@ -24,10 +26,17 @@ const vrNumerics = [
* @param obj
* @returns
*/
export default function cleanDenaturalizedDataset(obj: any): any {
export default function cleanDenaturalizedDataset(
obj: any,
options: {
StudyInstanceUID: string;
SeriesInstanceUID: string;
dataSourceConfig: unknown;
}
): any {
if (Array.isArray(obj)) {
const newAry = obj.map(o =>
isPrimitive(o) ? o : cleanDenaturalizedDataset(o)
isPrimitive(o) ? o : cleanDenaturalizedDataset(o, options)
);
return newAry;
} else if (isPrimitive(obj)) {
@ -38,6 +47,12 @@ export default function cleanDenaturalizedDataset(obj: any): any {
delete obj[key].Value;
} else if (Array.isArray(obj[key].Value) && obj[key].vr) {
if (obj[key].Value.length === 1 && obj[key].Value[0].BulkDataURI) {
dicomWebUtils.fixBulkDataURI(
obj[key].Value[0],
options,
options.dataSourceConfig
);
obj[key].BulkDataURI = obj[key].Value[0].BulkDataURI;
// prevent mixed-content blockage
@ -54,7 +69,9 @@ export default function cleanDenaturalizedDataset(obj: any): any {
} else if (vrNumerics.includes(obj[key].vr)) {
obj[key].Value = obj[key].Value.map(v => +v);
} else {
obj[key].Value = obj[key].Value.map(cleanDenaturalizedDataset);
obj[key].Value = obj[key].Value.map(entry =>
cleanDenaturalizedDataset(entry, options)
);
}
}
});

View File

@ -1,6 +1,12 @@
import { api } from 'dicomweb-client';
import { errorHandler, DicomMetadataStore } from '@ohif/core';
const { DICOMwebClient } = api;
DICOMwebClient._buildMultipartAcceptHeaderFieldValue = () => {
return '*/*';
};
/**
* create a DICOMwebClient object to be used by Dicom Microscopy Viewer
*

View File

@ -29,6 +29,7 @@ function OHIFCornerstoneVideoViewport({ displaySets }) {
controlsList="nodownload"
preload="auto"
className="w-full h-full"
crossOrigin="anonymous"
>
<source src={url} type={mimeType} />
<source src={url} type={mimeType} />

View File

@ -32,8 +32,8 @@
"peerDependencies": {
"@ohif/core": "^3.0.0",
"classnames": "^2.3.2",
"@cornerstonejs/core": "^0.47.1",
"@cornerstonejs/tools": "^0.67.4",
"@cornerstonejs/core": "^1.1.0",
"@cornerstonejs/tools": "^1.1.0",
"@ohif/extension-cornerstone-dicom-sr": "^3.0.0",
"dcmjs": "^0.29.5",
"lodash.debounce": "^4.17.21",

View File

@ -292,7 +292,8 @@ function PanelStudyBrowserTracking({
const tabs = _createStudyBrowserTabs(
StudyInstanceUIDs,
studyDisplayList,
displaySets
displaySets,
hangingProtocolService
);
// TODO: Should not fire this on "close"
@ -601,7 +602,8 @@ function _getComponentType(Modality) {
function _createStudyBrowserTabs(
primaryStudyInstanceUIDs,
studyDisplayList,
displaySets
displaySets,
hangingProtocolService
) {
const primaryStudies = [];
const recentStudies = [];
@ -615,9 +617,8 @@ function _createStudyBrowserTabs(
);
// Sort them
const sortedDisplaySetsForStudy = utils.sortBySeriesDate(
displaySetsForStudy
);
const dsSortFn = hangingProtocolService.getDisplaySetSortFunction();
displaySetsForStudy.sort(dsSortFn);
/* Sort by series number, then by series date
displaySetsForStudy.sort((a, b) => {

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import OHIF, { utils } from '@ohif/core';
@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next';
import { annotation } from '@cornerstonejs/tools';
import { useTrackedMeasurements } from './../getContextModule';
import { BaseVolumeViewport, Enums } from '@cornerstonejs/core';
const { formatDate } = utils;
@ -34,6 +35,7 @@ function TrackedCornerstoneViewport(props) {
const [trackedMeasurements] = useTrackedMeasurements();
const [isTracked, setIsTracked] = useState(false);
const [trackedMeasurementUID, setTrackedMeasurementUID] = useState(null);
const [viewportElem, setViewportElem] = useState(null);
const { trackedSeries } = trackedMeasurements.context;
const viewportId = viewportOptions.viewportId;
@ -55,6 +57,69 @@ function TrackedCornerstoneViewport(props) {
ManufacturerModelName,
} = displaySet.images[0];
const updateIsTracked = useCallback(() => {
const viewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
viewportIndex
);
if (viewport instanceof BaseVolumeViewport) {
// A current image id will only exist for volume viewports that can have measurements tracked.
// Typically these are those volume viewports for the series of acquisition.
const currentImageId = viewport?.getCurrentImageId();
if (!currentImageId) {
if (isTracked) {
setIsTracked(false);
}
return;
}
}
if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) {
setIsTracked(!isTracked);
}
}, [isTracked, trackedMeasurements, viewportIndex, SeriesInstanceUID]);
const onElementEnabled = useCallback(
evt => {
if (evt.detail.element !== viewportElem) {
// The VOLUME_VIEWPORT_NEW_VOLUME event allows updateIsTracked to reliably fetch the image id for a volume viewport.
evt.detail.element?.addEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
updateIsTracked
);
setViewportElem(evt.detail.element);
}
},
[updateIsTracked, viewportElem]
);
const onElementDisabled = useCallback(() => {
viewportElem?.removeEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
updateIsTracked
);
}, [updateIsTracked, viewportElem]);
useEffect(updateIsTracked, [updateIsTracked]);
useEffect(() => {
const { unsubscribe } = cornerstoneViewportService.subscribe(
cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED,
props => {
if (props.viewportIndex !== viewportIndex) {
return;
}
updateIsTracked();
}
);
return () => {
unsubscribe();
};
}, [updateIsTracked, viewportIndex]);
useEffect(() => {
if (isTracked) {
annotation.config.style.setViewportToolStyles(viewportId, {
@ -83,19 +148,6 @@ function TrackedCornerstoneViewport(props) {
};
}, [isTracked]);
// A current image id will only exist for viewports that can have measurements tracked.
// Typically these are stack viewports and those volume viewports for the series of acquisition.
const currentImageId = cornerstoneViewportService
.getCornerstoneViewport(viewportId)
?.getCurrentImageId();
if (currentImageId) {
if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) {
setIsTracked(!isTracked);
}
} else if (isTracked) {
setIsTracked(false);
}
function switchMeasurement(direction) {
const newTrackedMeasurementUID = _getNextMeasurementUID(
direction,
@ -121,7 +173,13 @@ function TrackedCornerstoneViewport(props) {
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
return <Component {...props} />;
return (
<Component
{...props}
onElementEnabled={onElementEnabled}
onElementDisabled={onElementDisabled}
/>
);
};
return (

View File

@ -189,16 +189,26 @@ function modeFactory({ modeConfiguration }) {
study: [],
series: [],
},
isValidMode: ({ modalities }) => {
isValidMode: ({ modalities, study }) => {
const modalities_list = modalities.split('\\');
const invalidModalities = ['SM'];
// there should be both CT and PT modalities and the modality should not be SM
return (
const isValid =
modalities_list.includes('CT') &&
modalities_list.includes('PT') &&
!invalidModalities.some(modality => modalities_list.includes(modality))
);
!invalidModalities.some(modality =>
modalities_list.includes(modality)
) &&
// This is study is a 4D study with PT and CT and not a 3D study for the tmtv
// mode, until we have a better way to identify 4D studies we will use the
// StudyInstanceUID to identify the study
// Todo: when we add the 4D mode which comes with a mechanism to identify
// 4D studies we can use that
study.studyInstanceUid !==
'1.3.6.1.4.1.12842.1.1.14.3.20220915.105557.468.2963630849';
// there should be both CT and PT modalities and the modality should not be SM
return isValid;
},
routes: [
{

View File

@ -147,7 +147,7 @@
]
},
"resolutions": {
"@cornerstonejs/core": "^0.47.1",
"@cornerstonejs/core": "^1.1.0",
"**/@babel/runtime": "^7.20.13",
"nth-check": "^2.1.1",
"trim-newlines": "^5.0.0",

View File

@ -32,7 +32,7 @@
},
"peerDependencies": {
"cornerstone-math": "0.1.9",
"@cornerstonejs/dicom-image-loader": "^0.6.6",
"@cornerstonejs/dicom-image-loader": "^0.6.8",
"@cornerstonejs/codec-charls": "^1.2.3",
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.2",

View File

@ -76,18 +76,24 @@ const bindings = [
keys: ['left'],
isEditable: true,
},
// {
// commandName: 'nextViewportDisplaySet',
// label: 'Next Series',
// keys: ['pageup'],
// isEditable: true,
// },
// {
// commandName: 'previousViewportDisplaySet',
// label: 'Previous Series',
// keys: ['pagedown'],
// isEditable: true,
// },
{
commandName: 'updateViewportDisplaySet',
commandOptions: {
direction: -1,
},
label: 'Previous Series',
keys: ['pageup'],
isEditable: true,
},
{
commandName: 'updateViewportDisplaySet',
commandOptions: {
direction: 1,
},
label: 'Next Series',
keys: ['pagedown'],
isEditable: true,
},
{
commandName: 'nextStage',
context: 'DEFAULT',

View File

@ -1033,6 +1033,23 @@ export default class HangingProtocolService extends PubSubService {
return this._matchViewport(useViewport, options);
}
/**
* Gets a sort function that is consistent with the display set sorting performed
* to match display sets to viewports.
* @returns a display set sort function
*/
public getDisplaySetSortFunction(): (
displaySetA: IDisplaySet,
displaySetB: IDisplaySet
) => number {
return (displaySetA, displaySetB) => {
const seriesA = this._getSeriesSortInfoForDisplaySetSort(displaySetA);
const seriesB = this._getSeriesSortInfoForDisplaySetSort(displaySetB);
return sortBy(this._getSeriesFieldForDisplaySetSort())(seriesA, seriesB);
};
}
/**
* Updates the viewports with the selected protocol stage.
*/
@ -1461,7 +1478,7 @@ export default class HangingProtocolService extends PubSubService {
sortingInfo: {
score: totalMatchScore,
study: study.StudyInstanceUID,
series: parseInt(displaySet.SeriesNumber),
...this._getSeriesSortInfoForDisplaySetSort(displaySet),
},
};
@ -1484,9 +1501,7 @@ export default class HangingProtocolService extends PubSubService {
name: 'study',
reverse: true,
},
{
name: 'series',
}
this._getSeriesFieldForDisplaySetSort()
);
matchingScores.sort((a, b) =>
sortingFunction(a.sortingInfo, b.sortingInfo)
@ -1506,6 +1521,19 @@ export default class HangingProtocolService extends PubSubService {
};
}
private _getSeriesSortInfoForDisplaySetSort(displaySet) {
return {
[this._getSeriesFieldForDisplaySetSort().name]:
displaySet.SeriesNumber != null
? parseInt(displaySet.SeriesNumber)
: parseInt(displaySet.seriesNumber),
};
}
private _getSeriesFieldForDisplaySetSort() {
return { name: 'series' };
}
/**
* Check if the next stage is available
* @return {Boolean} True if next stage is available or false otherwise

View File

@ -179,6 +179,24 @@ See the [`singlepart`](#singlepart) data source configuration option.
### DICOM Video
See the [`singlepart`](#singlepart) data source configuration option.
### BulkDataURI
The `bulkDataURI` configuration option allows the datasource to use the
bulkdata end points for retrieving metadata if originally was not included in the
response from the server. This is useful for the metadata information that
are big and can/should be retrieved in a separate request. In case the bulkData URI
is relative (instead of absolute) the `relativeResolution` option can be used to
specify the resolution of the relative URI. The possible values are `studies`, `series` and `instances`.
Certainly the knowledge of how the server is configured is required to use this option.
```js
bulkDataURI: {
enabled: true,
relativeResolution: 'series',
},
```
### Running DCM4CHEE
dcm4che is a collection of open source applications for healthcare enterprise

View File

@ -209,6 +209,13 @@ see [custom routes](./platform/services/ui/customization-service.md#customroutes
</details>
## DICOM Endpoints
In OHIF v3 there is a new end point that your DICOM server should be able to respond to
`WADO-RS GET studies/{studyInstanceUid}/series`
This is used in the viewer for fetching the series list for a study to use for the hanging protocol.
## LifeCycle Hooks
OHIF v2 had `preRegistration` hook for extensions for initialization. In OHIF v3 you have

View File

@ -490,7 +490,7 @@ are specific to the context used for where the menu is displayed.
The default cornerstone context menu can be customized by setting the
`cornerstoneContextMenu`. For a full example, see `findingsContextMenu`.
## Customizeable Cornerstone Viewport Click Behaviour
## Customizable Cornerstone Viewport Click Behaviour
The behaviour on clicking on the cornerstone viewport can be customized
by setting the `cornerstoneViewportClickCommands`. This is intended to

View File

@ -47,6 +47,7 @@
"react-outside-click-handler": "^1.3.0",
"react-select": "3.0.8",
"react-with-direction": "^1.3.1",
"react-window": "^1.8.9",
"swiper": "^8.4.2",
"webpack": "^5.81.0"
},

View File

@ -0,0 +1,8 @@
<svg width="19" height="19" viewBox="0 0 19 19" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<circle fill="#0944B3" cx="9.5" cy="9.5" r="9.5"/>
<g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="m5.188 5.187 8.625 8.625M13.813 5.187l-8.625 8.625"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 377 B

View File

@ -0,0 +1,9 @@
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h18v18H0z"/>
<g transform="translate(1 1)" stroke="#348CFD" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
<circle cx="5.565" cy="5.565" r="5.565"/>
<path d="M9.5 9.5 16 16"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 402 B

View File

@ -53,12 +53,14 @@ import checkboxUnchecked from './../../assets/icons/checkbox-unchecked.svg';
import iconAlertOutline from './../../assets/icons/icons-alert-outline.svg';
import iconAlertSmall from './../../assets/icons/icon-alert-small.svg';
import iconClose from './../../assets/icons/icon-close.svg';
import iconClearField from './../../assets/icons/icon-clear-field.svg';
import iconNextInactive from './../../assets/icons/icon-next-inactive.svg';
import iconNext from './../../assets/icons/icon-next.svg';
import iconPlay from './../../assets/icons/icon-play.svg';
import iconPause from './../../assets/icons/icon-pause.svg';
import iconPrevInactive from './../../assets/icons/icon-prev-inactive.svg';
import iconPrev from './../../assets/icons/icon-prev.svg';
import iconSearch from './../../assets/icons/icon-search.svg';
import iconStatusAlert from './../../assets/icons/icon-status-alert.svg';
import iconTransferring from './../../assets/icons/icon-transferring.svg';
import iconUpload from './../../assets/icons/icon-upload.svg';
@ -151,9 +153,11 @@ const ICONS = {
info: info,
'icon-alert-outline': iconAlertOutline,
'icon-alert-small': iconAlertSmall,
'icon-clear-field': iconClearField,
'icon-close': iconClose,
'icon-play': iconPlay,
'icon-pause': iconPause,
'icon-search': iconSearch,
'icon-status-alert': iconStatusAlert,
'icon-transferring': iconTransferring,
'info-action': infoAction,

View File

@ -5,8 +5,8 @@ input[type='range'] {
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
border: none;
height: 10px;
width: 10px;
height: 13px;
width: 13px;
border-radius: 50%;
background: #5acce6;
}
@ -14,8 +14,8 @@ input[type='range']::-webkit-slider-thumb {
input[type='range']::-moz-range-thumb {
-webkit-appearance: none;
border: none;
height: 10px;
width: 10px;
height: 13px;
width: 13px;
border-radius: 50%;
background: #5acce6;
}

View File

@ -23,7 +23,9 @@ const InputRange: React.FC<{
inputClassName?: string;
labelClassName?: string;
labelVariant?: string;
showLabel: boolean;
showLabel?: boolean;
labelPosition?: string;
trackColor?: string;
}> = ({
value,
onChange,
@ -36,6 +38,8 @@ const InputRange: React.FC<{
labelClassName,
labelVariant,
showLabel = true,
labelPosition = '',
trackColor,
}) => {
const [rangeValue, setRangeValue] = useState(value);
@ -63,6 +67,16 @@ const InputRange: React.FC<{
containerClassName ? containerClassName : ''
}`}
>
{showLabel && labelPosition === 'left' && (
<Typography
variant={labelVariant ?? 'subtitle'}
component="p"
className={classNames('w-8', labelClassName ?? 'text-white')}
>
{rangeValueForStr}
{unit}
</Typography>
)}
<input
type="range"
min={minValue}
@ -72,14 +86,16 @@ const InputRange: React.FC<{
inputClassName ? inputClassName : ''
}`}
style={{
background: `linear-gradient(to right, #5acce6 0%, #5acce6 ${rangeValuePercentage -
10}%, #3a3f99 ${rangeValuePercentage + 10}%, #3a3f99 100%)`,
background:
trackColor ||
`linear-gradient(to right, #5acce6 0%, #5acce6 ${rangeValuePercentage -
10}%, #3a3f99 ${rangeValuePercentage + 10}%, #3a3f99 100%)`,
}}
onChange={handleChange}
id="myRange"
step={step}
/>
{showLabel && (
{showLabel && (!labelPosition || labelPosition === 'right') && (
<Typography
variant={labelVariant ?? 'subtitle'}
component="p"

View File

@ -19,7 +19,7 @@ function LoadingIndicatorProgress({ className, textBlock, progress }) {
>
<Icon name="loading-ohif-mark" className="text-white w-12 h-12" />
<div className="w-48">
<ProgressLoadingBar></ProgressLoadingBar>
<ProgressLoadingBar progress={progress} />
</div>
{textBlock}
</div>

View File

@ -63,6 +63,7 @@ const Thumbnail = ({
src={imageSrc}
alt={imageAltText}
className="object-none min-h-32"
crossOrigin="anonymous"
/>
) : (
<div>{imageAltText}</div>

View File

@ -12,7 +12,10 @@ const ThumbnailList = ({
activeDisplaySetInstanceUIDs = [],
}) => {
return (
<div className="py-3 bg-black overflow-y-hidden ohif-scrollbar study-min-height">
<div
id="ohif-thumbnail-list"
className="py-3 bg-black overflow-y-hidden ohif-scrollbar study-min-height"
>
{thumbnails.map(
({
displaySetInstanceUID,

View File

@ -39,7 +39,7 @@ module.exports = {
main: '#3a3f99',
disabled: '#2b166b',
focus: '#5acce6',
placeholder: '#39383f'
placeholder: '#39383f',
},
secondary: {

View File

@ -65,10 +65,9 @@
"@ohif/ui": "^2.0.0",
"@types/react": "^17.0.38",
"classnames": "^2.3.2",
"config-point": "^0.4.8",
"core-js": "^3.16.1",
"cornerstone-math": "^0.1.9",
"@cornerstonejs/dicom-image-loader": "^0.6.6",
"@cornerstonejs/dicom-image-loader": "^0.6.8",
"@cornerstonejs/codec-charls": "^1.2.3",
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.2",

View File

@ -48,9 +48,9 @@ window.config = {
// wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
// new server
wadoUriRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
qidoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
wadoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
wadoUriRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
qidoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
wadoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
qidoSupportsIncludeField: false,
supportsReject: false,
@ -60,9 +60,14 @@ window.config = {
supportsFuzzyMatching: false,
supportsWildcard: true,
staticWado: true,
singlepart: 'bulkdata,video,pdf',
useBulkDataURI: false,
//requestTransferSyntaxUID: '1.2.840.10008.1.2.4.80'
singlepart: 'bulkdata,video',
// whether the data source should use retrieveBulkData to grab metadata,
// and in case of relative path, what would it be relative to, options
// are in the series level or study level (some servers like series some study)
bulkDataURI: {
enabled: true,
relativeResolution: 'studies',
},
},
},
{

View File

@ -16,13 +16,15 @@ window.config = {
sourceName: 'dicomweb',
configuration: {
name: 'DCM4CHEE',
wadoUriRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
qidoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
wadoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
wadoUriRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
qidoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
wadoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
qidoSupportsIncludeField: true,
imageRendering: 'wadors',
enableStudyLazyLoad: true,
useBulkDataURI: false,
bulkDataURI: {
enabled: false,
},
},
},
],

View File

@ -64,9 +64,9 @@ window.config = {
// qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
// wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
// new server
wadoUriRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
qidoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
wadoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
wadoUriRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
qidoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
wadoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
qidoSupportsIncludeField: false,
supportsReject: false,
imageRendering: 'wadors',

View File

@ -33,6 +33,12 @@ window.config = {
},
dicomUploadEnabled: true,
singlepart: 'pdf,video',
// whether the data source should use retrieveBulkData to grab metadata,
// and in case of relative path, what would it be relative to, options
// are in the series level or study level (some servers like series some study)
bulkDataURI: {
enabled: true,
},
},
},
{

View File

@ -29,10 +29,12 @@ window.config = {
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
useBulkDataURI: false,
supportsFuzzyMatching: true,
supportsWildcard: true,
dicomUploadEnabled: true,
bulkDataURI: {
enabled: false,
},
},
},
{

View File

@ -55,9 +55,9 @@ window.config = {
// qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
// wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
// new server
wadoUriRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
qidoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
wadoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
wadoUriRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
qidoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
wadoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
qidoSupportsIncludeField: false,
supportsReject: false,
imageRendering: 'wadors',

View File

@ -18,9 +18,10 @@ window.config = {
sourceName: 'dicomweb',
configuration: {
name: 'aws',
wadoUriRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
qidoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
wadoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
wadoUriRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
qidoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
wadoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
qidoSupportsIncludeField: false,
supportsReject: false,
imageRendering: 'wadors',
@ -29,7 +30,14 @@ window.config = {
supportsFuzzyMatching: false,
supportsWildcard: true,
staticWado: true,
singlepart: 'bulkdata,video,pdf',
singlepart: 'bulkdata,video',
// whether the data source should use retrieveBulkData to grab metadata,
// and in case of relative path, what would it be relative to, options
// are in the series level or study level (some servers like series some study)
bulkDataURI: {
enabled: true,
relativeResolution: 'studies',
},
},
},
{

View File

@ -11,9 +11,9 @@ window.config = {
dicomWeb: [
{
name: 'aws',
wadoUriRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
qidoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
wadoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
wadoUriRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
qidoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
wadoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
qidoSupportsIncludeField: true,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',

View File

@ -343,6 +343,7 @@ function WorkList({
const isValidMode = mode.isValidMode({
modalities: modalitiesToCheck,
study,
});
// TODO: Modes need a default/target route? We mostly support a single one for now.
// We should also be using the route path, but currently are not

View File

@ -50,7 +50,7 @@ module.exports = {
main: '#3a3f99',
disabled: '#2b166b',
focus: '#5acce6',
placeholder: '#39383f'
placeholder: '#39383f',
},
secondary: {

View File

@ -1302,7 +1302,7 @@
core-js-pure "^3.25.1"
regenerator-runtime "^0.13.11"
"@babel/runtime@7.17.9", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.6", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
"@babel/runtime@7.17.9", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.6", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.21.0"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
@ -1418,10 +1418,10 @@
resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
"@cornerstonejs/adapters@^0.6.0":
version "0.6.0"
resolved "https://registry.npmjs.org/@cornerstonejs/adapters/-/adapters-0.6.0.tgz#9b2efdabb0d596d53ae4854556956b668cca7535"
integrity sha512-bzOwtOX0EfJ/PufPq1mONPU+HmVQf+pA/78+mbHuV8bvznM1IkSzc2h0WYsLXTdVEkZrGwgWCwrOiYGfsNW1tQ==
"@cornerstonejs/adapters@^1.1.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@cornerstonejs/adapters/-/adapters-1.1.0.tgz#6476ff9075291a10204992cffbc178c67a785ed4"
integrity sha512-v2uj9uGd0mEuQfPvKBuohgNM+EDOAqOexnI0yHfAOIQkzA+FPdifr5vRoH6TKkSk403GZh8ASGeoyleavEifmQ==
dependencies:
"@babel/runtime-corejs2" "^7.17.8"
dcmjs "^0.29.5"
@ -1469,43 +1469,43 @@
resolved "https://registry.npmjs.org/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.2.tgz#e96721d56f6ec96f7f95c16321d88cc8467d8d81"
integrity sha512-lgdvBvvNezleY+4pIe2ceUsJzlZe/0PipdeubQ3vZZOz3xxtHHMR1XFCl4fgd8gosR8COHuD7h6q+MwgrwBsng==
"@cornerstonejs/core@^0.47.1":
version "0.47.1"
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-0.47.1.tgz#8bbb9d5d8a6cd8a4c0fbf0b20bc0f56808d4fa40"
integrity sha512-bA69qo2WkMd5lkFFegYQ1UWHbnOADF0TD6DHiOnabFxHMQYZLB9CkCy4lT/tPAYL2w9WKm6b52ScUPNLTirWeA==
"@cornerstonejs/core@^0.47.3", "@cornerstonejs/core@^1.1.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@cornerstonejs/core/-/core-1.1.0.tgz#bedac4d7583be931f289e5e645a1531b605cbb83"
integrity sha512-YLbb47rCrxf04XZ1+qR1ATnbKlV6WXOsEc4pAmty5WfTqL9zlmSx72IoXx034m39Nz9JvZB+VUN8MKvVrYv3bw==
dependencies:
"@kitware/vtk.js" "27.3.1"
detect-gpu "^5.0.22"
gl-matrix "^3.4.3"
lodash.clonedeep "4.5.0"
"@cornerstonejs/dicom-image-loader@^0.6.6":
version "0.6.6"
resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-0.6.6.tgz#086b14e93e67923ea999d7f80c42bf766db3918f"
integrity sha512-pdJ5f7/JEX4BAXR2keAAGf6RUfuyzB3vhEvk0T4yDv960TdpT4RZqRjs5acZMs9bkGqY8zAb+c9+Gdz5vypfvg==
"@cornerstonejs/dicom-image-loader@^0.6.8":
version "0.6.8"
resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-0.6.8.tgz#e9d5c24959d74981b3ccca3641617b2c34106355"
integrity sha512-wYKPXprDYgHrsz2tNf2o0UL0Mr4YcUF4xalQqdLxfrgyUExQKHoDigXmEAwNVokSlS7rNz6NplLXeTbNJ2QSwg==
dependencies:
"@cornerstonejs/codec-charls" "^1.2.3"
"@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2"
"@cornerstonejs/codec-openjpeg" "^1.2.2"
"@cornerstonejs/codec-openjph" "^2.4.2"
"@cornerstonejs/core" "^0.47.1"
"@cornerstonejs/core" "^0.47.3"
dicom-parser "^1.8.9"
pako "^2.0.4"
uuid "^9.0.0"
"@cornerstonejs/streaming-image-volume-loader@^0.20.4":
version "0.20.4"
resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-0.20.4.tgz#f8d727f2ae7dc0ae3c8975bad2a4f5365743127f"
integrity sha512-Zkf/HKatWGf3GPb+0yR7wNPqBHfKXQznpcKsqwFbbMu/SNp7dBcCuvqRVXO5BrJOfnyDeeDpB2+2XRocBnighg==
"@cornerstonejs/streaming-image-volume-loader@^1.1.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-1.1.0.tgz#bd2fa9cddc8088c6e6501be6c3bba0f722478078"
integrity sha512-HGHm3WHUxwzsTEkcGwscwFOuxZxeVvRZ1MkcSpkGVgiL15NE8L47FKBLP9Ph2kdv+5DC3IOQuZV502mhi2unag==
dependencies:
"@cornerstonejs/core" "^0.47.1"
"@cornerstonejs/core" "^1.1.0"
"@cornerstonejs/tools@^0.67.4":
version "0.67.4"
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-0.67.4.tgz#5dfc2d3bacc2be600496cd4e230b1749de59b001"
integrity sha512-qeCCtK/xioEBv7l1zqicWQrHdzkFOX4fGNZ5D/TMkQMa+Ed5Zip8gDAcYTbmUvbYXdvkzhoOZeLcPVhoAkDykw==
"@cornerstonejs/tools@^1.1.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@cornerstonejs/tools/-/tools-1.1.0.tgz#8c59f50899127505fd1e7e775ccf264ebddb8ae5"
integrity sha512-1OhTAycShG5BoAH5x1XUN9TrSdL9hwB5jTBgm0t/nJGVj/DJ29RMXvakgX8HAv1K2acrKRl8JcJCV2VS9OAHtQ==
dependencies:
"@cornerstonejs/core" "^0.47.1"
"@cornerstonejs/core" "^1.1.0"
lodash.clonedeep "4.5.0"
lodash.get "^4.4.2"
@ -7461,14 +7461,6 @@ config-chain@^1.1.11:
ini "^1.3.4"
proto-list "~1.2.1"
config-point@^0.4.8:
version "0.4.9"
resolved "https://registry.npmjs.org/config-point/-/config-point-0.4.9.tgz#ec4594d04235438d5852e5ab33bde60aeffd18db"
integrity sha512-2QyqD2eb2iURURZNW6MAcNcT2e1Pr1VXwCaMgEQM0f/i+WaUCLZOdPjFtqF7oIT1rzPXNoMfl3KK2hbNcy5dXw==
dependencies:
"@babel/runtime" "^7.14.6"
json5 "^2.2.0"
configstore@^5.0.1:
version "5.0.1"
resolved "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96"
@ -13975,7 +13967,7 @@ memfs@^3.1.2, memfs@^3.4.1, memfs@^3.4.3:
dependencies:
fs-monkey "^1.0.3"
memoize-one@^5.0.0:
"memoize-one@>=3.1.1 <6", memoize-one@^5.0.0:
version "5.2.1"
resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
@ -17518,6 +17510,14 @@ react-waypoint@^10.3.0:
prop-types "^15.0.0"
react-is "^17.0.1 || ^18.0.0"
react-window@^1.8.9:
version "1.8.9"
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.9.tgz#24bc346be73d0468cdf91998aac94e32bc7fa6a8"
integrity sha512-+Eqx/fj1Aa5WnhRfj9dJg4VYATGwIUP2ItwItiJ6zboKWA6EX3lYDAXfGF2hyNqplEprhbtjbipiADEcwQ823Q==
dependencies:
"@babel/runtime" "^7.0.0"
memoize-one ">=3.1.1 <6"
react-with-direction@^1.3.1:
version "1.4.0"
resolved "https://registry.npmjs.org/react-with-direction/-/react-with-direction-1.4.0.tgz#ebdf64d685d0650ce966e872e6431ad5a2485444"