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:
Alireza 2023-05-29 09:04:49 -04:00 committed by GitHub
parent b684d80426
commit c7bcf1134d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 290 additions and 156 deletions

View File

@ -7,12 +7,13 @@ async function checkAndLoadContourData(instance, datasource) {
return Promise.reject('Invalid instance object or ROIContourSequence'); return Promise.reject('Invalid instance object or ROIContourSequence');
} }
const promises = []; const promisesMap = new Map();
let counter = 0;
for (const ROIContour of instance.ROIContourSequence) { for (const ROIContour of instance.ROIContourSequence) {
const referencedROINumber = ROIContour.ReferencedROINumber;
if (!ROIContour || !ROIContour.ContourSequence) { if (!ROIContour || !ROIContour.ContourSequence) {
return Promise.reject('Invalid ROIContour or ContourSequence'); promisesMap.set(referencedROINumber, [Promise.resolve([])]);
continue;
} }
for (const Contour of ROIContour.ContourSequence) { for (const Contour of ROIContour.ContourSequence) {
@ -21,9 +22,15 @@ async function checkAndLoadContourData(instance, datasource) {
} }
const contourData = Contour.ContourData; const contourData = Contour.ContourData;
counter++;
if (Array.isArray(contourData)) { 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) { } else if (contourData && contourData.BulkDataURI) {
const bulkDataURI = contourData.BulkDataURI; const bulkDataURI = contourData.BulkDataURI;
@ -44,53 +51,59 @@ async function checkAndLoadContourData(instance, datasource) {
SOPInstanceUID: instance.SOPInstanceUID, SOPInstanceUID: instance.SOPInstanceUID,
}); });
promises.push(bulkDataPromise); promisesMap.has(referencedROINumber)
? promisesMap.get(referencedROINumber).push(bulkDataPromise)
: promisesMap.set(referencedROINumber, [bulkDataPromise]);
} else { } else {
return Promise.reject(`Invalid ContourData: ${contourData}`); 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 const resolvedPromisesMap = new Map();
let index = 0; for (const [key, promiseArray] of promisesMap.entries()) {
instance.ROIContourSequence.forEach((ROIContour, roiIndex) => { resolvedPromisesMap.set(key, await Promise.allSettled(promiseArray));
ROIContour.ContourSequence.forEach((Contour, contourIndex) => { }
const promise = resolvedPromises[index++];
if (promise.status === 'fulfilled') { instance.ROIContourSequence.forEach(ROIContour => {
const uint8Array = new Uint8Array(promise.value); try {
const textDecoder = new TextDecoder(); const referencedROINumber = ROIContour.ReferencedROINumber;
const dataUint8Array = textDecoder.decode(uint8Array); const resolvedPromises = resolvedPromisesMap.get(referencedROINumber);
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 = [];
while (numbersParsed !== numSlashes + 1) { if (ROIContour.ContourSequence) {
const str = dataUint8Array.substring(startIndex, endIndex); ROIContour.ContourSequence.forEach((Contour, index) => {
let value = parseFloat(str); const promise = resolvedPromises[index];
if (promise.status === 'fulfilled') {
ContourData.push(value); if (
startIndex = endIndex + 1; Array.isArray(promise.value) &&
endIndex = dataUint8Array.indexOf('\\', startIndex); promise.value.every(Number.isFinite)
endIndex === -1 ? (endIndex = dataUint8Array.length) : endIndex; ) {
numbersParsed++; // 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' '@ohif/extension-cornerstone.utilityModule.common'
); );
const dataSource = extensionManager.getActiveDataSource()[0]; const dataSource = extensionManager.getActiveDataSource()[0];
const { useBulkDataURI } = dataSource.getConfig?.() || {}; const { bulkDataURI } = dataSource.getConfig?.() || {};
const { dicomLoaderService } = utilityModule.exports; const { dicomLoaderService } = utilityModule.exports;
const imageIdSopInstanceUidPairs = _getImageIdSopInstanceUidPairsForDisplaySet( const imageIdSopInstanceUidPairs = _getImageIdSopInstanceUidPairsForDisplaySet(
@ -116,7 +129,7 @@ export default async function loadRTStruct(
rtStructDisplaySet.isLoaded = true; rtStructDisplaySet.isLoaded = true;
let instance = rtStructDisplaySet.instance; let instance = rtStructDisplaySet.instance;
if (!useBulkDataURI) { if (!bulkDataURI || !bulkDataURI.enabled) {
const segArrayBuffer = await dicomLoaderService.findDicomDataPromise( const segArrayBuffer = await dicomLoaderService.findDicomDataPromise(
rtStructDisplaySet, rtStructDisplaySet,
null, null,

View File

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

View File

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

View File

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

View File

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

View File

@ -3,14 +3,14 @@ import { ServicesManager, utils, Types } from '@ohif/core';
import { import {
ContextMenuController, ContextMenuController,
defaultContextMenu, defaultContextMenu,
} from './CustomizeableContextMenu'; } from './CustomizableContextMenu';
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser'; import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
import reuseCachedLayouts from './utils/reuseCachedLayouts'; import reuseCachedLayouts from './utils/reuseCachedLayouts';
import findViewportsByPosition, { import findViewportsByPosition, {
findOrCreateViewport as layoutFindOrCreate, findOrCreateViewport as layoutFindOrCreate,
} from './findViewportsByPosition'; } from './findViewportsByPosition';
import { ContextMenuProps } from './CustomizeableContextMenu/types'; import { ContextMenuProps } from './CustomizableContextMenu/types';
import { NavigateHistory } from './types/commandModuleTypes'; import { NavigateHistory } from './types/commandModuleTypes';
import { history } from '@ohif/viewer'; import { history } from '@ohif/viewer';

View File

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

View File

@ -1,10 +1,4 @@
import { import { utils } from '@ohif/core';
DicomMetadataStore,
IWebApiDataSource,
utils,
errorHandler,
classes,
} from '@ohif/core';
/** /**
* Generates a URL that can be used for direct retrieve of the bulkdata * Generates a URL that can be used for direct retrieve of the bulkdata
@ -57,31 +51,19 @@ const getDirectURL = (config, params) => {
const BulkDataURI = const BulkDataURI =
(value && value.BulkDataURI) || (value && value.BulkDataURI) ||
`series/${SeriesInstanceUID}/instances/${SOPInstanceUID}${defaultPath}`; `series/${SeriesInstanceUID}/instances/${SOPInstanceUID}${defaultPath}`;
const hasQuery = BulkDataURI.indexOf('?') != -1; const hasQuery = BulkDataURI.indexOf('?') !== -1;
const hasAccept = BulkDataURI.indexOf('accept=') != -1; const hasAccept = BulkDataURI.indexOf('accept=') !== -1;
const acceptUri = const acceptUri =
BulkDataURI + BulkDataURI +
(hasAccept ? '' : (hasQuery ? '&' : '?') + `accept=${defaultType}`); (hasAccept ? '' : (hasQuery ? '&' : '?') + `accept=${defaultType}`);
if (BulkDataURI.indexOf('http') === 0) {
if (tag === 'PixelData' || tag === 'EncapsulatedDocument') { if (tag === 'PixelData' || tag === 'EncapsulatedDocument') {
return `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/rendered`; return `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/rendered`;
} else {
return acceptUri;
}
} }
if (BulkDataURI.indexOf('/') === 0) {
return wadoRoot + acceptUri; // 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.
if (BulkDataURI.indexOf('series/') == 0) { return acceptUri;
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);
}; };
export default getDirectURL; export default getDirectURL;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { HangingProtocolService, utils } from '@ohif/core'; import { utils } from '@ohif/core';
import { import {
StudyBrowser, StudyBrowser,
useImageViewer, useImageViewer,

View File

@ -189,16 +189,26 @@ function modeFactory({ modeConfiguration }) {
study: [], study: [],
series: [], series: [],
}, },
isValidMode: ({ modalities }) => { isValidMode: ({ modalities, study }) => {
const modalities_list = modalities.split('\\'); const modalities_list = modalities.split('\\');
const invalidModalities = ['SM']; const invalidModalities = ['SM'];
// there should be both CT and PT modalities and the modality should not be SM const isValid =
return (
modalities_list.includes('CT') && modalities_list.includes('CT') &&
modalities_list.includes('PT') && 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: [ routes: [
{ {

View File

@ -179,6 +179,24 @@ See the [`singlepart`](#singlepart) data source configuration option.
### DICOM Video ### DICOM Video
See the [`singlepart`](#singlepart) data source configuration option. 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 ### Running DCM4CHEE
dcm4che is a collection of open source applications for healthcare enterprise dcm4che is a collection of open source applications for healthcare enterprise

View File

@ -209,6 +209,13 @@ see [custom routes](./platform/services/ui/customization-service.md#customroutes
</details> </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 ## LifeCycle Hooks
OHIF v2 had `preRegistration` hook for extensions for initialization. In OHIF v3 you have OHIF v2 had `preRegistration` hook for extensions for initialization. In OHIF v3 you have

View File

@ -490,7 +490,7 @@ are specific to the context used for where the menu is displayed.
The default cornerstone context menu can be customized by setting the The default cornerstone context menu can be customized by setting the
`cornerstoneContextMenu`. For a full example, see `findingsContextMenu`. `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 The behaviour on clicking on the cornerstone viewport can be customized
by setting the `cornerstoneViewportClickCommands`. This is intended to by setting the `cornerstoneViewportClickCommands`. This is intended to

View File

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

View File

@ -65,7 +65,6 @@
"@ohif/ui": "^2.0.0", "@ohif/ui": "^2.0.0",
"@types/react": "^17.0.38", "@types/react": "^17.0.38",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"config-point": "^0.4.8",
"core-js": "^3.16.1", "core-js": "^3.16.1",
"cornerstone-math": "^0.1.9", "cornerstone-math": "^0.1.9",
"@cornerstonejs/dicom-image-loader": "^0.6.8", "@cornerstonejs/dicom-image-loader": "^0.6.8",

View File

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

View File

@ -22,7 +22,9 @@ window.config = {
qidoSupportsIncludeField: true, qidoSupportsIncludeField: true,
imageRendering: 'wadors', imageRendering: 'wadors',
enableStudyLazyLoad: true, enableStudyLazyLoad: true,
useBulkDataURI: false, bulkDataURI: {
enabled: false,
},
}, },
}, },
], ],

View File

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

View File

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

View File

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

View File

@ -1302,7 +1302,7 @@
core-js-pure "^3.25.1" core-js-pure "^3.25.1"
regenerator-runtime "^0.13.11" 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" version "7.21.0"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
@ -7461,14 +7461,6 @@ config-chain@^1.1.11:
ini "^1.3.4" ini "^1.3.4"
proto-list "~1.2.1" 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: configstore@^5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" resolved "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96"