Merge branch 'v3-stable' into customizable-TS
This commit is contained in:
commit
56633a67d9
@ -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,
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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,
|
||||
};
|
||||
@ -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,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 };
|
||||
3
extensions/default/src/DicomWebDataSource/utils/index.ts
Normal file
3
extensions/default/src/DicomWebDataSource/utils/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { fixBulkDataURI } from './fixBulkDataURI';
|
||||
|
||||
export { fixBulkDataURI };
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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({});
|
||||
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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
|
||||
*
|
||||
|
||||
@ -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} />
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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: [
|
||||
{
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
8
platform/ui/src/assets/icons/icon-clear-field.svg
Normal file
8
platform/ui/src/assets/icons/icon-clear-field.svg
Normal 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 |
9
platform/ui/src/assets/icons/icon-search.svg
Normal file
9
platform/ui/src/assets/icons/icon-search.svg
Normal 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 |
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -63,6 +63,7 @@ const Thumbnail = ({
|
||||
src={imageSrc}
|
||||
alt={imageAltText}
|
||||
className="object-none min-h-32"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
) : (
|
||||
<div>{imageAltText}</div>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -39,7 +39,7 @@ module.exports = {
|
||||
main: '#3a3f99',
|
||||
disabled: '#2b166b',
|
||||
focus: '#5acce6',
|
||||
placeholder: '#39383f'
|
||||
placeholder: '#39383f',
|
||||
},
|
||||
|
||||
secondary: {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -29,10 +29,12 @@ window.config = {
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
useBulkDataURI: false,
|
||||
supportsFuzzyMatching: true,
|
||||
supportsWildcard: true,
|
||||
dicomUploadEnabled: true,
|
||||
bulkDataURI: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -50,7 +50,7 @@ module.exports = {
|
||||
main: '#3a3f99',
|
||||
disabled: '#2b166b',
|
||||
focus: '#5acce6',
|
||||
placeholder: '#39383f'
|
||||
placeholder: '#39383f',
|
||||
},
|
||||
|
||||
secondary: {
|
||||
|
||||
66
yarn.lock
66
yarn.lock
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user