fix(staticwado): SM and RT and update the server with new data (#3422)
* fix bugs for the RT for the new demo * fix SM with the static-wado server * remove thumbnail from tmtv * migration guide * fix stability for the rt struct * apply review comments * add loading indicator to SM * pdf works * try to fix relative bulkData * fix the rest * fix preflight for the SM * fix typo * apply review comments * yarn lock
This commit is contained in:
parent
b684d80426
commit
c7bcf1134d
@ -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,
|
||||
|
||||
@ -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,
|
||||
};
|
||||
@ -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;
|
||||
|
||||
@ -373,13 +374,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';
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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?
|
||||
@ -165,13 +193,7 @@ class DicomMicroscopyViewport extends Component {
|
||||
metadata: inst,
|
||||
});
|
||||
|
||||
// NOTE: depending on different data source, image.ImageType sometimes
|
||||
// is a string, not a string array.
|
||||
const imageType =
|
||||
typeof image.ImageType === 'string'
|
||||
? image.ImageType.split('\\')
|
||||
: image.ImageType;
|
||||
const imageFlavor = imageType[2];
|
||||
const imageFlavor = image.ImageType[2];
|
||||
if (imageFlavor === 'VOLUME' || imageFlavor === 'THUMBNAIL') {
|
||||
volumeImages.push(image);
|
||||
}
|
||||
@ -182,7 +204,7 @@ class DicomMicroscopyViewport extends Component {
|
||||
client,
|
||||
metadata: volumeImages,
|
||||
retrieveRendered: false,
|
||||
controls: ['overview', 'position', 'zoom'],
|
||||
controls: ['overview', 'position'],
|
||||
};
|
||||
|
||||
this.viewer = new microscopyViewer(options);
|
||||
@ -237,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(
|
||||
@ -316,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,7 +47,11 @@ 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) {
|
||||
obj[key].BulkDataURI = obj[key].Value[0].BulkDataURI;
|
||||
obj[key].Value[0] = dicomWebUtils.fixBulkDataURI(
|
||||
obj[key].Value[0],
|
||||
options,
|
||||
options.dataSourceConfig
|
||||
);
|
||||
|
||||
// prevent mixed-content blockage
|
||||
if (
|
||||
@ -54,7 +67,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
|
||||
*
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { HangingProtocolService, utils } from '@ohif/core';
|
||||
import { utils } from '@ohif/core';
|
||||
import {
|
||||
StudyBrowser,
|
||||
useImageViewer,
|
||||
|
||||
@ -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: [
|
||||
{
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -65,7 +65,6 @@
|
||||
"@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.8",
|
||||
|
||||
@ -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,8 +60,14 @@ window.config = {
|
||||
supportsFuzzyMatching: false,
|
||||
supportsWildcard: true,
|
||||
staticWado: true,
|
||||
singlepart: 'bulkdata,video,pdf',
|
||||
useBulkDataURI: false,
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -22,7 +22,9 @@ window.config = {
|
||||
qidoSupportsIncludeField: true,
|
||||
imageRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
useBulkDataURI: false,
|
||||
bulkDataURI: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -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
|
||||
|
||||
10
yarn.lock
10
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.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.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==
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user