diff --git a/extensions/cornerstone-dicom-rt/src/loadRTStruct.js b/extensions/cornerstone-dicom-rt/src/loadRTStruct.js index 52e1bd7c7..613c2fb65 100644 --- a/extensions/cornerstone-dicom-rt/src/loadRTStruct.js +++ b/extensions/cornerstone-dicom-rt/src/loadRTStruct.js @@ -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, diff --git a/extensions/default/src/CustomizeableContextMenu/ContextMenuController.tsx b/extensions/default/src/CustomizableContextMenu/ContextMenuController.tsx similarity index 100% rename from extensions/default/src/CustomizeableContextMenu/ContextMenuController.tsx rename to extensions/default/src/CustomizableContextMenu/ContextMenuController.tsx diff --git a/extensions/default/src/CustomizeableContextMenu/ContextMenuItemsBuilder.test.js b/extensions/default/src/CustomizableContextMenu/ContextMenuItemsBuilder.test.js similarity index 100% rename from extensions/default/src/CustomizeableContextMenu/ContextMenuItemsBuilder.test.js rename to extensions/default/src/CustomizableContextMenu/ContextMenuItemsBuilder.test.js diff --git a/extensions/default/src/CustomizeableContextMenu/ContextMenuItemsBuilder.ts b/extensions/default/src/CustomizableContextMenu/ContextMenuItemsBuilder.ts similarity index 100% rename from extensions/default/src/CustomizeableContextMenu/ContextMenuItemsBuilder.ts rename to extensions/default/src/CustomizableContextMenu/ContextMenuItemsBuilder.ts diff --git a/extensions/default/src/CustomizeableContextMenu/defaultContextMenu.ts b/extensions/default/src/CustomizableContextMenu/defaultContextMenu.ts similarity index 100% rename from extensions/default/src/CustomizeableContextMenu/defaultContextMenu.ts rename to extensions/default/src/CustomizableContextMenu/defaultContextMenu.ts diff --git a/extensions/default/src/CustomizeableContextMenu/index.ts b/extensions/default/src/CustomizableContextMenu/index.ts similarity index 75% rename from extensions/default/src/CustomizeableContextMenu/index.ts rename to extensions/default/src/CustomizableContextMenu/index.ts index 7dc08dd3d..d0abca069 100644 --- a/extensions/default/src/CustomizeableContextMenu/index.ts +++ b/extensions/default/src/CustomizableContextMenu/index.ts @@ -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, }; diff --git a/extensions/default/src/CustomizeableContextMenu/types.ts b/extensions/default/src/CustomizableContextMenu/types.ts similarity index 100% rename from extensions/default/src/CustomizeableContextMenu/types.ts rename to extensions/default/src/CustomizableContextMenu/types.ts diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index 01943417e..00f82841e 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -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 diff --git a/extensions/default/src/DicomWebDataSource/utils/fixBulkDataURI.ts b/extensions/default/src/DicomWebDataSource/utils/fixBulkDataURI.ts new file mode 100644 index 000000000..1a408b597 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/utils/fixBulkDataURI.ts @@ -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 }; diff --git a/extensions/default/src/DicomWebDataSource/utils/index.ts b/extensions/default/src/DicomWebDataSource/utils/index.ts new file mode 100644 index 000000000..2132253fe --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/utils/index.ts @@ -0,0 +1,3 @@ +import { fixBulkDataURI } from './fixBulkDataURI'; + +export { fixBulkDataURI }; diff --git a/extensions/default/src/commandsModule.ts b/extensions/default/src/commandsModule.ts index 230a82943..958f34b60 100644 --- a/extensions/default/src/commandsModule.ts +++ b/extensions/default/src/commandsModule.ts @@ -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'; diff --git a/extensions/default/src/index.ts b/extensions/default/src/index.ts index c56f39b7e..2b023d415 100644 --- a/extensions/default/src/index.ts +++ b/extensions/default/src/index.ts @@ -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, }; diff --git a/extensions/default/src/utils/getDirectURL.js b/extensions/default/src/utils/getDirectURL.js index 4580b8d30..c3905e9f2 100644 --- a/extensions/default/src/utils/getDirectURL.js +++ b/extensions/default/src/utils/getDirectURL.js @@ -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; diff --git a/extensions/dicom-microscopy/src/DicomMicroscopyViewport.tsx b/extensions/dicom-microscopy/src/DicomMicroscopyViewport.tsx index 4c562ece1..dc794a896 100644 --- a/extensions/dicom-microscopy/src/DicomMicroscopyViewport.tsx +++ b/extensions/dicom-microscopy/src/DicomMicroscopyViewport.tsx @@ -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 { ) : (
)} + {this.state.isLoaded ? null : ( + + )}
); } diff --git a/extensions/dicom-microscopy/src/components/ViewportOverlay/index.tsx b/extensions/dicom-microscopy/src/components/ViewportOverlay/index.tsx index bbe863e71..e4bb67424 100644 --- a/extensions/dicom-microscopy/src/components/ViewportOverlay/index.tsx +++ b/extensions/dicom-microscopy/src/components/ViewportOverlay/index.tsx @@ -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({}); diff --git a/extensions/dicom-microscopy/src/utils/cleanDenaturalizedDataset.ts b/extensions/dicom-microscopy/src/utils/cleanDenaturalizedDataset.ts index 91d72e3e2..49d135558 100644 --- a/extensions/dicom-microscopy/src/utils/cleanDenaturalizedDataset.ts +++ b/extensions/dicom-microscopy/src/utils/cleanDenaturalizedDataset.ts @@ -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) + ); } } }); diff --git a/extensions/dicom-microscopy/src/utils/dicomWebClient.ts b/extensions/dicom-microscopy/src/utils/dicomWebClient.ts index 8174d6c26..bb8b1f2e7 100644 --- a/extensions/dicom-microscopy/src/utils/dicomWebClient.ts +++ b/extensions/dicom-microscopy/src/utils/dicomWebClient.ts @@ -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 * diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx index 99eaa6a5e..171a27bff 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx @@ -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, diff --git a/modes/tmtv/src/index.js b/modes/tmtv/src/index.js index e4cfc5c25..aae993bbd 100644 --- a/modes/tmtv/src/index.js +++ b/modes/tmtv/src/index.js @@ -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: [ { diff --git a/platform/docs/docs/configuration/dataSources/dicom-web.md b/platform/docs/docs/configuration/dataSources/dicom-web.md index 2736ffc9d..24b2c16b3 100644 --- a/platform/docs/docs/configuration/dataSources/dicom-web.md +++ b/platform/docs/docs/configuration/dataSources/dicom-web.md @@ -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 diff --git a/platform/docs/docs/migration-guide.md b/platform/docs/docs/migration-guide.md index cad1b8851..f836c8607 100644 --- a/platform/docs/docs/migration-guide.md +++ b/platform/docs/docs/migration-guide.md @@ -209,6 +209,13 @@ see [custom routes](./platform/services/ui/customization-service.md#customroutes +## 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 diff --git a/platform/docs/docs/platform/services/ui/customization-service.md b/platform/docs/docs/platform/services/ui/customization-service.md index b59bf10d9..516df2d24 100644 --- a/platform/docs/docs/platform/services/ui/customization-service.md +++ b/platform/docs/docs/platform/services/ui/customization-service.md @@ -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 diff --git a/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx b/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx index 2c18a8bf4..e66d1de7f 100644 --- a/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx +++ b/platform/ui/src/components/LoadingIndicatorProgress/LoadingIndicatorProgress.tsx @@ -19,7 +19,7 @@ function LoadingIndicatorProgress({ className, textBlock, progress }) { >
- +
{textBlock} diff --git a/platform/viewer/package.json b/platform/viewer/package.json index 1f5407fa1..4a487787a 100644 --- a/platform/viewer/package.json +++ b/platform/viewer/package.json @@ -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", diff --git a/platform/viewer/public/config/default.js b/platform/viewer/public/config/default.js index ff93f31a1..8ff91875b 100644 --- a/platform/viewer/public/config/default.js +++ b/platform/viewer/public/config/default.js @@ -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', + }, }, }, { diff --git a/platform/viewer/public/config/demo.js b/platform/viewer/public/config/demo.js index 59f01cc9c..27f7bc53f 100644 --- a/platform/viewer/public/config/demo.js +++ b/platform/viewer/public/config/demo.js @@ -22,7 +22,9 @@ window.config = { qidoSupportsIncludeField: true, imageRendering: 'wadors', enableStudyLazyLoad: true, - useBulkDataURI: false, + bulkDataURI: { + enabled: false, + }, }, }, ], diff --git a/platform/viewer/public/config/local_dcm4chee.js b/platform/viewer/public/config/local_dcm4chee.js index 682501fff..763604ffd 100644 --- a/platform/viewer/public/config/local_dcm4chee.js +++ b/platform/viewer/public/config/local_dcm4chee.js @@ -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, + }, }, }, { diff --git a/platform/viewer/public/config/local_orthanc.js b/platform/viewer/public/config/local_orthanc.js index 140f4d516..80469a291 100644 --- a/platform/viewer/public/config/local_orthanc.js +++ b/platform/viewer/public/config/local_orthanc.js @@ -29,10 +29,12 @@ window.config = { imageRendering: 'wadors', thumbnailRendering: 'wadors', enableStudyLazyLoad: true, - useBulkDataURI: false, supportsFuzzyMatching: true, supportsWildcard: true, dicomUploadEnabled: true, + bulkDataURI: { + enabled: false, + }, }, }, { diff --git a/platform/viewer/src/routes/WorkList/WorkList.tsx b/platform/viewer/src/routes/WorkList/WorkList.tsx index df2c30376..f5c914111 100644 --- a/platform/viewer/src/routes/WorkList/WorkList.tsx +++ b/platform/viewer/src/routes/WorkList/WorkList.tsx @@ -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 diff --git a/yarn.lock b/yarn.lock index d414a7a5a..e0b3eea49 100644 --- a/yarn.lock +++ b/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"