diff --git a/extensions/default/jest.config.js b/extensions/default/jest.config.js index fc9df72ade5..5ae4f4bf101 100644 --- a/extensions/default/jest.config.js +++ b/extensions/default/jest.config.js @@ -5,6 +5,7 @@ module.exports = { moduleNameMapper: { ...base.moduleNameMapper, '@ohif/(.*)': '/../../platform/$1/src', + 'dicomweb-client': '/../../node_modules/dicomweb-client', '^@cornerstonejs/(.*)$': '/../../node_modules/@cornerstonejs/$1/dist/esm', }, // rootDir: "../.." @@ -12,4 +13,4 @@ module.exports = { // //`/platform/${pack.name}/**/*.spec.js` // "/platform/app/**/*.test.js" // ] -}; +}; \ No newline at end of file diff --git a/extensions/default/src/DicomJSONDataSource/index.js b/extensions/default/src/DataSources/DicomJSONDataSource/index.js similarity index 92% rename from extensions/default/src/DicomJSONDataSource/index.js rename to extensions/default/src/DataSources/DicomJSONDataSource/index.js index 18949f6e661..b02ec7c7a97 100644 --- a/extensions/default/src/DicomJSONDataSource/index.js +++ b/extensions/default/src/DataSources/DicomJSONDataSource/index.js @@ -2,8 +2,7 @@ import { DicomMetadataStore, IWebApiDataSource } from '@ohif/core'; import OHIF from '@ohif/core'; import qs from 'query-string'; -import getImageId from '../DicomWebDataSource/utils/getImageId'; -import getDirectURL from '../utils/getDirectURL'; +import {getDirectURL, getImageId} from '../utils'; const metadataProvider = OHIF.classes.MetadataProvider; @@ -60,6 +59,7 @@ const findStudies = (key, value) => { }; function createDicomJSONApi(dicomJsonConfig) { + const dicomJsonConfigCopy = JSON.parse(JSON.stringify(dicomJsonConfig)); const implementation = { initialize: async ({ query, url }) => { if (!url) { @@ -89,7 +89,11 @@ function createDicomJSONApi(dicomJsonConfig) { series.instances.forEach(instance => { const { metadata: naturalizedDicom } = instance; - const imageId = getImageId({ instance, config: dicomJsonConfig }); + const imageId = getImageId( + instance, + undefined, + dicomJsonConfig + ); const { query } = qs.parseUrl(instance.url); @@ -231,7 +235,11 @@ function createDicomJSONApi(dicomJsonConfig) { const obj = { ...modifiedMetadata, url: instance.url, - imageId: getImageId({ instance, config: dicomJsonConfig }), + imageId: getImageId( + instance, + undefined, + dicomJsonConfig + ), ...series, ...study, }; @@ -280,20 +288,26 @@ function createDicomJSONApi(dicomJsonConfig) { const NumberOfFrames = instance.NumberOfFrames || 1; const instances = instanceMap.get(instance.SOPInstanceUID) || [instance]; for (let i = 0; i < NumberOfFrames; i++) { - const imageId = getImageId({ - instance: instances[Math.min(i, instances.length - 1)], - frame: NumberOfFrames > 1 ? i : undefined, - config: dicomJsonConfig, - }); + const imageId = getImageId( + instances[Math.min(i, instances.length - 1)], + NumberOfFrames > 1 ? i : undefined, + dicomJsonConfig, + ); imageIds.push(imageId); } }); return imageIds; }, - getImageIdsForInstance({ instance, frame }) { - const imageIds = getImageId({ instance, frame }); - return imageIds; + getImageIdsForInstance({ instance, frame = undefined }) { + return getImageId({ + instance, + frame, + config: dicomJsonConfig, + }); + }, + getConfig() { + return dicomJsonConfigCopy; }, getStudyInstanceUIDs: ({ params, query }) => { const url = query.get('url'); diff --git a/extensions/default/src/DicomLocalDataSource/index.js b/extensions/default/src/DataSources/DicomLocalDataSource/index.js similarity index 96% rename from extensions/default/src/DicomLocalDataSource/index.js rename to extensions/default/src/DataSources/DicomLocalDataSource/index.js index 462fb875113..d9ae9e80e71 100644 --- a/extensions/default/src/DicomLocalDataSource/index.js +++ b/extensions/default/src/DataSources/DicomLocalDataSource/index.js @@ -43,6 +43,7 @@ const customSort = (seriesA, seriesB) => { function createDicomLocalApi(dicomLocalConfig) { const { name } = dicomLocalConfig; + const dicomLocalConfigCopy = JSON.parse(JSON.stringify(dicomLocalConfig)); const implementation = { initialize: ({ params, query }) => {}, @@ -194,14 +195,14 @@ function createDicomLocalApi(dicomLocalConfig) { if (NumberOfFrames > 1) { // in multiframe we start at frame 1 for (let i = 1; i <= NumberOfFrames; i++) { - const imageId = this.getImageIdsForInstance({ + const imageId = implementation.getImageIdsForInstance({ instance, frame: i, }); imageIds.push(imageId); } } else { - const imageId = this.getImageIdsForInstance({ instance }); + const imageId = implementation.getImageIdsForInstance({ instance }); imageIds.push(imageId); } }); @@ -234,6 +235,9 @@ function createDicomLocalApi(dicomLocalConfig) { deleteStudyMetadataPromise() { console.log('deleteStudyMetadataPromise not implemented'); }, + getConfig() { + return dicomLocalConfigCopy; + }, getStudyInstanceUIDs: ({ params, query }) => { const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params; const queryStudyInstanceUIDs = query.getAll('StudyInstanceUIDs'); diff --git a/extensions/default/src/DicomWebDataSource/dcm4cheeReject.js b/extensions/default/src/DataSources/DicomWebDataSource/dcm4cheeReject.js similarity index 100% rename from extensions/default/src/DicomWebDataSource/dcm4cheeReject.js rename to extensions/default/src/DataSources/DicomWebDataSource/dcm4cheeReject.js diff --git a/extensions/default/src/DicomWebDataSource/exampleInstances.js b/extensions/default/src/DataSources/DicomWebDataSource/exampleInstances.js similarity index 100% rename from extensions/default/src/DicomWebDataSource/exampleInstances.js rename to extensions/default/src/DataSources/DicomWebDataSource/exampleInstances.js diff --git a/extensions/default/src/DicomWebDataSource/index.ts b/extensions/default/src/DataSources/DicomWebDataSource/index.ts similarity index 53% rename from extensions/default/src/DicomWebDataSource/index.ts rename to extensions/default/src/DataSources/DicomWebDataSource/index.ts index afc781b21ca..cbcc933cd69 100644 --- a/extensions/default/src/DicomWebDataSource/index.ts +++ b/extensions/default/src/DataSources/DicomWebDataSource/index.ts @@ -1,25 +1,24 @@ import { api } from 'dicomweb-client'; -import { DicomMetadataStore, IWebApiDataSource, utils, errorHandler, classes } from '@ohif/core'; +import { IWebApiDataSource, utils, errorHandler, classes } from '@ohif/core'; import { mapParams, - search as qidoSearch, + qidoSearch, seriesInStudy, processResults, processSeriesResults, -} from './qido.js'; -import dcm4cheeReject from './dcm4cheeReject.js'; - -import getImageId from './utils/getImageId.js'; -import dcmjs from 'dcmjs'; -import { retrieveStudyMetadata, deleteStudyMetadataPromise } from './retrieveStudyMetadata.js'; -import StaticWadoClient from './utils/StaticWadoClient'; -import getDirectURL from '../utils/getDirectURL'; -import { fixBulkDataURI } from './utils/fixBulkDataURI'; - -const { DicomMetaDictionary, DicomDict } = dcmjs.data; + getImageId, + deleteStudyMetadataPromise, + StaticWadoClient, + getDirectURL, + denaturalizeDataset, + DicomWebConfig, + retrieveFullSeriesMetadata, + retrieveSeriesMetadataAsync, + generateAuthorizationHeader, +} from '../utils'; -const { naturalizeDataset, denaturalizeDataset } = DicomMetaDictionary; +import dcm4cheeReject from './dcm4cheeReject.js'; const ImplementationClassUID = '2.25.270695996825855179949881587723571202391.2.0.0'; const ImplementationVersionName = 'OHIF-3.11.0'; @@ -27,76 +26,6 @@ const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1'; const metadataProvider = classes.MetadataProvider; -export type DicomWebConfig = { - /** Data source name */ - name: string; - // wadoUriRoot - Legacy? (potentially unused/replaced) - /** Base URL to use for QIDO requests */ - qidoRoot?: string; - wadoRoot?: string; // - Base URL to use for WADO requests - wadoUri?: string; // - Base URL to use for WADO URI requests - qidoSupportsIncludeField?: boolean; // - Whether QIDO supports the "Include" option to request additional fields in response - imageRendering?: string; // - wadors | ? (unsure of where/how this is used) - thumbnailRendering?: string; - /** - wadors - render using the wadors fetch. The full image is retrieved and rendered in cornerstone to thumbnail size png and returned as binary data to the src attribute of the image tag. - for example, - thumbnailDirect - get the direct url endpoint for the thumbnail as the image src (eg not authentication required). - for example, - thumbnail - render using the thumbnail endpoint on wadors using bulkDataURI, passing authentication params to the url. - rendered - should use the rendered endpoint instead of the thumbnail endpoint -*/ - /** Whether the server supports reject calls (i.e. DCM4CHEE) */ - supportsReject?: boolean; - /** indicates if the retrieves can fetch singlepart. Options are bulkdata, video, image, or true */ - singlepart?: boolean | string; - /** Transfer syntax to request from the server */ - requestTransferSyntaxUID?: string; - acceptHeader?: string[]; // - Accept header to use for requests - /** Whether to omit quotation marks for multipart requests */ - omitQuotationForMultipartRequest?: boolean; - /** Whether the server supports fuzzy matching */ - supportsFuzzyMatching?: boolean; - /** Whether the server supports wildcard matching */ - supportsWildcard?: boolean; - /** Whether the server supports the native DICOM model */ - supportsNativeDICOMModel?: boolean; - /** Whether to enable request tag */ - enableRequestTag?: boolean; - /** Whether to enable study lazy loading */ - enableStudyLazyLoad?: boolean; - /** Whether to enable bulkDataURI */ - bulkDataURI?: BulkDataURIConfig; - /** Function that is called after the configuration is initialized */ - onConfiguration: (config: DicomWebConfig, params) => DicomWebConfig; - /** Whether to use the static WADO client */ - staticWado?: boolean; - /** User authentication service */ - userAuthenticationService: Record; -}; - -export type BulkDataURIConfig = { - /** Enable bulkdata uri configuration */ - enabled?: boolean; - /** - * Remove the startsWith string. - * This is used to correct reverse proxied URLs by removing the startsWith path - */ - startsWith?: string; - /** - * Adds this prefix path. Only used if the startsWith is defined and has - * been removed. This allows replacing the base path. - */ - prefixWith?: string; - /** Transform the bulkdata path. Used to replace a portion of the path */ - transform?: (uri: string) => string; - /** - * Adds relative resolution to the path handling. - * series is the default, as the metadata retrieved is series level. - */ - relativeResolution?: 'studies' | 'series'; -}; - /** * Creates a DICOM Web API based on the provided configuration. * @@ -105,13 +34,12 @@ export type BulkDataURIConfig = { */ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) { const { userAuthenticationService } = servicesManager.services; - let dicomWebConfigCopy, - qidoConfig, + const dicomWebConfigCopy = JSON.parse(JSON.stringify(dicomWebConfig)); + let qidoConfig, wadoConfig, qidoDicomWebClient, wadoDicomWebClient, - getAuthorizationHeader, - generateWadoHeader; + getAuthorizationHeader; // Default to enabling bulk data retrieves, with no other customization as // this is part of hte base standard. dicomWebConfig.bulkDataURI ||= { enabled: true }; @@ -125,31 +53,9 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) { }); } - dicomWebConfigCopy = JSON.parse(JSON.stringify(dicomWebConfig)); - getAuthorizationHeader = () => { - const xhrRequestHeaders = {}; - const authHeaders = userAuthenticationService.getAuthorizationHeader(); - if (authHeaders && authHeaders.Authorization) { - xhrRequestHeaders.Authorization = authHeaders.Authorization; - } - return xhrRequestHeaders; - }; - - generateWadoHeader = () => { - const authorizationHeader = getAuthorizationHeader(); - //Generate accept header depending on config params - const formattedAcceptHeader = utils.generateAcceptHeader( - dicomWebConfig.acceptHeader, - dicomWebConfig.requestTransferSyntaxUID, - dicomWebConfig.omitQuotationForMultipartRequest - ); - - return { - ...authorizationHeader, - Accept: formattedAcceptHeader, - }; - }; + return generateAuthorizationHeader(userAuthenticationService); + } qidoConfig = { url: dicomWebConfig.qidoRoot, @@ -409,70 +315,23 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) { sortFunction, madeInClient ) => { - const enableStudyLazyLoad = false; - wadoDicomWebClient.headers = generateWadoHeader(); - // data is all SOPInstanceUIDs - const data = await retrieveStudyMetadata( + const getImageIdsForInstance = implementation.getImageIdsForInstance; + const retrieveDependencies = { + qidoDicomWebClient, wadoDicomWebClient, + metadataProvider, + dicomWebConfig, + userAuthenticationService, + getImageIdsForInstance + } + return retrieveFullSeriesMetadata ( StudyInstanceUID, - enableStudyLazyLoad, filters, sortCriteria, sortFunction, - dicomWebConfig - ); - - // first naturalize the data - const naturalizedInstancesMetadata = data.map(naturalizeDataset); - - const seriesSummaryMetadata = {}; - const instancesPerSeries = {}; - - naturalizedInstancesMetadata.forEach(instance => { - if (!seriesSummaryMetadata[instance.SeriesInstanceUID]) { - seriesSummaryMetadata[instance.SeriesInstanceUID] = { - StudyInstanceUID: instance.StudyInstanceUID, - StudyDescription: instance.StudyDescription, - SeriesInstanceUID: instance.SeriesInstanceUID, - SeriesDescription: instance.SeriesDescription, - SeriesNumber: instance.SeriesNumber, - SeriesTime: instance.SeriesTime, - SOPClassUID: instance.SOPClassUID, - ProtocolName: instance.ProtocolName, - Modality: instance.Modality, - }; - } - - if (!instancesPerSeries[instance.SeriesInstanceUID]) { - instancesPerSeries[instance.SeriesInstanceUID] = []; - } - - const imageId = implementation.getImageIdsForInstance({ - instance, - }); - - instance.imageId = imageId; - instance.wadoRoot = dicomWebConfig.wadoRoot; - instance.wadoUri = dicomWebConfig.wadoUri; - - metadataProvider.addImageIdToUIDs(imageId, { - StudyInstanceUID, - SeriesInstanceUID: instance.SeriesInstanceUID, - SOPInstanceUID: instance.SOPInstanceUID, - }); - - instancesPerSeries[instance.SeriesInstanceUID].push(instance); - }); - - // grab all the series metadata - const seriesMetadata = Object.values(seriesSummaryMetadata); - DicomMetadataStore.addSeriesMetadata(seriesMetadata, madeInClient); - - Object.keys(instancesPerSeries).forEach(seriesInstanceUID => - DicomMetadataStore.addInstances(instancesPerSeries[seriesInstanceUID], madeInClient) + madeInClient, + retrieveDependencies ); - - return seriesSummaryMetadata; }, _retrieveSeriesMetadataAsync: async ( @@ -483,140 +342,24 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) { madeInClient = false, returnPromises = false ) => { - const enableStudyLazyLoad = true; - wadoDicomWebClient.headers = generateWadoHeader(); - // Get Series - const { preLoadData: seriesSummaryMetadata, promises: seriesPromises } = - await retrieveStudyMetadata( - wadoDicomWebClient, - StudyInstanceUID, - enableStudyLazyLoad, - filters, - sortCriteria, - sortFunction, - dicomWebConfig - ); - - /** - * Adds the retrieve bulkdata function to naturalized DICOM data. - * This is done recursively, for sub-sequences. - */ - const addRetrieveBulkDataNaturalized = (naturalized, instance = naturalized) => { - if (!naturalized) { - return naturalized; - } - for (const key of Object.keys(naturalized)) { - const value = naturalized[key]; - - if (Array.isArray(value) && typeof value[0] === 'object') { - // Fix recursive values - const validValues = value.filter(Boolean); - validValues.forEach(child => addRetrieveBulkDataNaturalized(child, instance)); - continue; - } - - // 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) { - // handle the scenarios where bulkDataURI is relative path - fixBulkDataURI(value, instance, dicomWebConfig); - // Provide a method to fetch bulkdata - value.retrieveBulkData = retrieveBulkData.bind(qidoDicomWebClient, value); - } - } - return naturalized; - }; - - /** - * naturalizes the dataset, and adds a retrieve bulkdata method - * to any values containing BulkDataURI. - * @param {*} instance - * @returns naturalized dataset, with retrieveBulkData methods - */ - const addRetrieveBulkData = instance => { - const naturalized = naturalizeDataset(instance); - - // if we know the server doesn't use bulkDataURI, then don't - if (!dicomWebConfig.bulkDataURI?.enabled) { - return naturalized; - } - - return addRetrieveBulkDataNaturalized(naturalized); - }; - - // Async load series, store as retrieved - function storeInstances(instances) { - const naturalizedInstances = instances.map(addRetrieveBulkData); - - // Adding instanceMetadata to OHIF MetadataProvider - naturalizedInstances.forEach(instance => { - instance.wadoRoot = dicomWebConfig.wadoRoot; - instance.wadoUri = dicomWebConfig.wadoUri; - - const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance; - const numberOfFrames = instance.NumberOfFrames || 1; - // Process all frames consistently, whether single or multiframe - for (let i = 0; i < numberOfFrames; i++) { - const frameNumber = i + 1; - const frameImageId = implementation.getImageIdsForInstance({ - instance, - frame: frameNumber, - }); - // Add imageId specific mapping to this data as the URL isn't necessarily WADO-URI. - metadataProvider.addImageIdToUIDs(frameImageId, { - StudyInstanceUID, - SeriesInstanceUID, - SOPInstanceUID, - frameNumber: numberOfFrames > 1 ? frameNumber : undefined, - }); - } - - // Adding imageId to each instance - // Todo: This is not the best way I can think of to let external - // metadata handlers know about the imageId that is stored in the store - const imageId = implementation.getImageIdsForInstance({ - instance, - }); - instance.imageId = imageId; - }); - - DicomMetadataStore.addInstances(naturalizedInstances, madeInClient); - } - - function setSuccessFlag() { - const study = DicomMetadataStore.getStudy(StudyInstanceUID); - if (!study) { - return; - } - study.isLoaded = true; - } - - // Google Cloud Healthcare doesn't return StudyInstanceUID, so we need to add - // it manually here - seriesSummaryMetadata.forEach(aSeries => { - aSeries.StudyInstanceUID = StudyInstanceUID; - }); - - DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata, madeInClient); - - const seriesDeliveredPromises = seriesPromises.map(promise => { - if (!returnPromises) { - promise?.start(); - } - return promise.then(instances => { - storeInstances(instances); - }); - }); - - if (returnPromises) { - Promise.all(seriesDeliveredPromises).then(() => setSuccessFlag()); - return seriesPromises; - } else { - await Promise.all(seriesDeliveredPromises); - setSuccessFlag(); + const getImageIdsForInstance = implementation.getImageIdsForInstance; + const retrieveDependencies = { + qidoDicomWebClient, + wadoDicomWebClient, + metadataProvider, + dicomWebConfig, + userAuthenticationService, + getImageIdsForInstance } - - return seriesSummaryMetadata; + return retrieveSeriesMetadataAsync( + StudyInstanceUID, + filters, + sortCriteria, + sortFunction, + retrieveDependencies, + madeInClient, + returnPromises + ); }, deleteStudyMetadataPromise, getImageIdsForDisplaySet(displaySet) { @@ -632,14 +375,14 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) { if (NumberOfFrames > 1) { for (let frame = 1; frame <= NumberOfFrames; frame++) { - const imageId = this.getImageIdsForInstance({ + const imageId = implementation.getImageIdsForInstance({ instance, frame, }); imageIds.push(imageId); } } else { - const imageId = this.getImageIdsForInstance({ instance }); + const imageId = implementation.getImageIdsForInstance({ instance }); imageIds.push(imageId); } }); @@ -647,12 +390,11 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) { return imageIds; }, getImageIdsForInstance({ instance, frame = undefined }) { - const imageIds = getImageId({ + return getImageId( instance, frame, - config: dicomWebConfig, - }); - return imageIds; + dicomWebConfig, + ); }, getConfig() { return dicomWebConfigCopy; diff --git a/extensions/default/src/DataSources/DicomWebMinimalDataSource/dcm4cheeReject.js b/extensions/default/src/DataSources/DicomWebMinimalDataSource/dcm4cheeReject.js new file mode 100644 index 00000000000..094e330f641 --- /dev/null +++ b/extensions/default/src/DataSources/DicomWebMinimalDataSource/dcm4cheeReject.js @@ -0,0 +1,45 @@ +export default function (wadoRoot, getAuthrorizationHeader) { + return { + series: (StudyInstanceUID, SeriesInstanceUID) => { + return new Promise((resolve, reject) => { + // Reject because of Quality. (Seems the most sensible out of the options) + const CodeValueAndCodeSchemeDesignator = `113001%5EDCM`; + + const url = `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/reject/${CodeValueAndCodeSchemeDesignator}`; + + const xhr = new XMLHttpRequest(); + xhr.open('POST', url, true); + + const headers = getAuthrorizationHeader(); + + for (const key in headers) { + xhr.setRequestHeader(key, headers[key]); + } + + //Send the proper header information along with the request + // TODO -> Auth when we re-add authorization. + + console.log(xhr); + + xhr.onreadystatechange = function () { + //Call a function when the state changes. + if (xhr.readyState == 4) { + switch (xhr.status) { + case 200: + case 204: + resolve(xhr.responseText); + + break; + case 404: + reject('Your dataSource does not support reject functionality'); + default: + reject(`Unexpected status code: ${xhr.status}`); + break; + } + } + }; + xhr.send(); + }); + }, + }; +} diff --git a/extensions/default/src/DataSources/DicomWebMinimalDataSource/exampleInstances.js b/extensions/default/src/DataSources/DicomWebMinimalDataSource/exampleInstances.js new file mode 100644 index 00000000000..9e179794967 --- /dev/null +++ b/extensions/default/src/DataSources/DicomWebMinimalDataSource/exampleInstances.js @@ -0,0 +1,280 @@ +export default [ + { + '00080005': { vr: 'CS', Value: ['ISO_IR 100'] }, + '00080008': { vr: 'CS', Value: ['ORIGINAL', 'PRIMARY', 'LOCALIZER'] }, + '00080016': { vr: 'UI', Value: ['1.2.840.10008.5.1.4.1.1.2'] }, + '00080018': { + vr: 'UI', + Value: ['1.3.6.1.4.1.25403.345050719074.3824.20170126082902.6'], + }, + '00080020': { vr: 'DA', Value: ['20141125'] }, + '00080021': { vr: 'DA', Value: ['20141125'] }, + '00080022': { vr: 'DA', Value: ['20141125'] }, + '00080023': { vr: 'DA', Value: ['20141125'] }, + '00080030': { vr: 'TM', Value: ['094528.000'] }, + '00080031': { vr: 'TM', Value: ['094604.688'] }, + '00080032': { vr: 'TM', Value: ['094623.600'] }, + '00080033': { vr: 'TM', Value: ['094623.600'] }, + '00080050': { vr: 'SH', Value: ['000092218'] }, + '00080060': { vr: 'CS', Value: ['CT'] }, + '00080070': { vr: 'LO', Value: ['TOSHIBA'] }, + '00080080': { vr: 'LO', Value: ['Precision Imaging Metrics'] }, + '00080090': { vr: 'PN' }, + '00081010': { vr: 'SH' }, + '00081030': { vr: 'LO', Value: ['DFCI CT CHEST W CONTRAST 6023'] }, + '00081032': { + vr: 'SQ', + Value: [ + { + '00080100': { vr: 'SH', Value: ['6023'] }, + '00080102': { vr: 'SH', Value: ['GEIIS'] }, + '00080103': { vr: 'SH', Value: ['0'] }, + '00080104': { vr: 'LO', Value: ['DFCI CT CHEST W CONTRAST 6023'] }, + }, + ], + }, + '0008103E': { vr: 'LO', Value: ['2.0'] }, + '00081040': { vr: 'LO' }, + '00081070': { vr: 'PN' }, + '00081090': { vr: 'LO', Value: ['Aquilion'] }, + '00081110': { + vr: 'SQ', + Value: [ + { + '00081150': { vr: 'UI', Value: ['1.2.840.100008.3.1.2.3.1'] }, + '00081155': { + vr: 'UI', + Value: ['1.3.6.1.4.1.25403.345050719074.3824.20170126082902.4'], + }, + }, + ], + }, + '00100010': { vr: 'PN', Value: [{ Alphabetic: 'Venus' }] }, + '00100020': { vr: 'LO', Value: ['0000005'] }, + '00100021': { vr: 'LO', Value: ['001R74:20050625:205502036:195212'] }, + '00100030': { vr: 'DA' }, + '00100040': { vr: 'CS', Value: ['F'] }, + '00101000': { vr: 'LO' }, + '00101010': { vr: 'AS' }, + '00101020': { vr: 'DS' }, + '00101030': { vr: 'DS' }, + '00104000': { vr: 'LT' }, + '00180015': { vr: 'CS', Value: ['CHEST_TO_PELVIS'] }, + '00180022': { vr: 'CS', Value: ['SCANOSCOPE'] }, + '00180050': { vr: 'DS', Value: [2.0] }, + '00180060': { vr: 'DS', Value: [120.0] }, + '00180090': { vr: 'DS', Value: [1000.0] }, + '00181000': { vr: 'LO' }, + '00181020': { vr: 'LO', Value: ['V4.86ER003'] }, + '00181030': { vr: 'LO', Value: ['Chest / Abdomen/Pelvis 5mm'] }, + '00181100': { vr: 'DS', Value: [1000.0] }, + '00181120': { vr: 'DS', Value: [0.0] }, + '00181130': { vr: 'DS', Value: [102.0] }, + '00181140': { vr: 'CS', Value: ['CW'] }, + '00181150': { vr: 'IS', Value: [6840] }, + '00181151': { vr: 'IS', Value: [100] }, + '00181152': { vr: 'IS', Value: [600] }, + '00181160': { vr: 'SH', Value: ['LARGE'] }, + '00181170': { vr: 'IS', Value: [12] }, + '00181190': { vr: 'DS', Value: [1.6, 1.4] }, + '00181210': { vr: 'SH', Value: ['FL03'] }, + '00185100': { vr: 'CS', Value: ['FFS'] }, + '0020000D': { + vr: 'UI', + Value: ['1.3.6.1.4.1.25403.345050719074.3824.20170126082902.1'], + }, + '0020000E': { + vr: 'UI', + Value: ['1.3.6.1.4.1.25403.345050719074.3824.20170126082902.2'], + }, + '00200010': { vr: 'SH' }, + '00200011': { vr: 'IS', Value: [1] }, + '00200012': { vr: 'IS', Value: [2] }, + '00200013': { vr: 'IS', Value: [2] }, + '00200020': { vr: 'CS', Value: ['F', 'P'] }, + '00200032': { vr: 'DS', Value: [-1.7e-4, -512.0, 1925.0] }, + '00200037': { vr: 'DS', Value: [0.0, 0.0, -1.0, 0.0, 1.0, -0.0] }, + '00200052': { + vr: 'UI', + Value: ['1.3.6.1.4.1.25403.345050719074.3824.20170126082902.5'], + }, + '00201040': { vr: 'LO' }, + '00201041': { vr: 'DS', Value: [342.0] }, + '00280002': { vr: 'US', Value: [1] }, + '00280004': { vr: 'CS', Value: ['MONOCHROME2'] }, + '00280010': { vr: 'US', Value: [512] }, + '00280011': { vr: 'US', Value: [512] }, + '00280030': { vr: 'DS', Value: [2.0, 2.0] }, + '00280100': { vr: 'US', Value: [16] }, + '00280101': { vr: 'US', Value: [16] }, + '00280102': { vr: 'US', Value: [15] }, + '00280103': { vr: 'US', Value: [1] }, + '00281050': { vr: 'DS', Value: [110.0] }, + '00281051': { vr: 'DS', Value: [320.0] }, + '00281052': { vr: 'DS', Value: [0.0] }, + '00281053': { vr: 'DS', Value: [1.0] }, + '00321033': { vr: 'LO', Value: ['OUTDFRAD'] }, + '00400002': { vr: 'DA', Value: ['20141125'] }, + '00400003': { vr: 'TM', Value: ['091000'] }, + '00400004': { vr: 'DA', Value: ['20141125'] }, + '00400005': { vr: 'TM', Value: ['094000.000'] }, + '00400244': { vr: 'DA', Value: ['20141125'] }, + '00400245': { vr: 'TM', Value: ['094528.000'] }, + '00400253': { vr: 'SH', Value: ['3708'] }, + '00400260': { + vr: 'SQ', + Value: [ + { + '00080100': { vr: 'SH', Value: ['6035'] }, + '00080102': { vr: 'SH', Value: ['CCG_CSTemp'] }, + '00080104': { vr: 'LO', Value: ['6035/DFCT2 CT 3-SITES W/OC'] }, + }, + ], + }, + '00402017': { vr: 'LO', Value: ['14159097'] }, + '7FE00010': { + vr: 'OW', + BulkDataURI: + 'http://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs/studies/1.3.6.1.4.1.25403.345050719074.3824.20170126082902.1/series/1.3.6.1.4.1.25403.345050719074.3824.20170126082902.2/instances/1.3.6.1.4.1.25403.345050719074.3824.20170126082902.6', + }, + }, + { + '00080005': { vr: 'CS', Value: ['ISO_IR 100'] }, + '00080008': { vr: 'CS', Value: ['ORIGINAL', 'PRIMARY', 'LOCALIZER'] }, + '00080016': { vr: 'UI', Value: ['1.2.840.10008.5.1.4.1.1.2'] }, + '00080018': { + vr: 'UI', + Value: ['1.3.6.1.4.1.25403.345050719074.3824.20170126082902.3'], + }, + '00080020': { vr: 'DA', Value: ['20141125'] }, + '00080021': { vr: 'DA', Value: ['20141125'] }, + '00080022': { vr: 'DA', Value: ['20141125'] }, + '00080023': { vr: 'DA', Value: ['20141125'] }, + '00080030': { vr: 'TM', Value: ['094528.000'] }, + '00080031': { vr: 'TM', Value: ['094604.688'] }, + '00080032': { vr: 'TM', Value: ['094557.250'] }, + '00080033': { vr: 'TM', Value: ['094557.250'] }, + '00080050': { vr: 'SH', Value: ['000092218'] }, + '00080060': { vr: 'CS', Value: ['CT'] }, + '00080070': { vr: 'LO', Value: ['TOSHIBA'] }, + '00080080': { vr: 'LO', Value: ['Precision Imaging Metrics'] }, + '00080090': { vr: 'PN' }, + '00081010': { vr: 'SH' }, + '00081030': { vr: 'LO', Value: ['DFCI CT CHEST W CONTRAST 6023'] }, + '00081032': { + vr: 'SQ', + Value: [ + { + '00080100': { vr: 'SH', Value: ['6023'] }, + '00080102': { vr: 'SH', Value: ['GEIIS'] }, + '00080103': { vr: 'SH', Value: ['0'] }, + '00080104': { vr: 'LO', Value: ['DFCI CT CHEST W CONTRAST 6023'] }, + }, + ], + }, + '0008103E': { vr: 'LO', Value: ['2.0'] }, + '00081040': { vr: 'LO' }, + '00081070': { vr: 'PN' }, + '00081090': { vr: 'LO', Value: ['Aquilion'] }, + '00081110': { + vr: 'SQ', + Value: [ + { + '00081150': { vr: 'UI', Value: ['1.2.840.100008.3.1.2.3.1'] }, + '00081155': { + vr: 'UI', + Value: ['1.3.6.1.4.1.25403.345050719074.3824.20170126082902.4'], + }, + }, + ], + }, + '00100010': { vr: 'PN', Value: [{ Alphabetic: 'Venus' }] }, + '00100020': { vr: 'LO', Value: ['0000005'] }, + '00100021': { vr: 'LO', Value: ['001R74:20050625:205502036:195212'] }, + '00100030': { vr: 'DA' }, + '00100040': { vr: 'CS', Value: ['F'] }, + '00101000': { vr: 'LO' }, + '00101010': { vr: 'AS' }, + '00101020': { vr: 'DS' }, + '00101030': { vr: 'DS' }, + '00104000': { vr: 'LT' }, + '00180015': { vr: 'CS', Value: ['CHEST_TO_PELVIS'] }, + '00180022': { vr: 'CS', Value: ['SCANOSCOPE'] }, + '00180050': { vr: 'DS', Value: [2.0] }, + '00180060': { vr: 'DS', Value: [120.0] }, + '00180090': { vr: 'DS', Value: [1000.0] }, + '00181000': { vr: 'LO' }, + '00181020': { vr: 'LO', Value: ['V4.86ER003'] }, + '00181030': { vr: 'LO', Value: ['Chest / Abdomen/Pelvis 5mm'] }, + '00181100': { vr: 'DS', Value: [1000.0] }, + '00181120': { vr: 'DS', Value: [0.0] }, + '00181130': { vr: 'DS', Value: [102.0] }, + '00181140': { vr: 'CS', Value: ['CW'] }, + '00181150': { vr: 'IS', Value: [6857] }, + '00181151': { vr: 'IS', Value: [50] }, + '00181152': { vr: 'IS', Value: [300] }, + '00181160': { vr: 'SH', Value: ['LARGE'] }, + '00181170': { vr: 'IS', Value: [6] }, + '00181190': { vr: 'DS', Value: [1.6, 1.4] }, + '00181210': { vr: 'SH', Value: ['FL03'] }, + '00185100': { vr: 'CS', Value: ['FFS'] }, + '0020000D': { + vr: 'UI', + Value: ['1.3.6.1.4.1.25403.345050719074.3824.20170126082902.1'], + }, + '0020000E': { + vr: 'UI', + Value: ['1.3.6.1.4.1.25403.345050719074.3824.20170126082902.2'], + }, + '00200010': { vr: 'SH' }, + '00200011': { vr: 'IS', Value: [1] }, + '00200012': { vr: 'IS', Value: [1] }, + '00200013': { vr: 'IS', Value: [1] }, + '00200020': { vr: 'CS', Value: ['L', 'F'] }, + '00200032': { vr: 'DS', Value: [-512.0, 1.7e-4, 1925.0] }, + '00200037': { vr: 'DS', Value: [1.0, 0.0, 0.0, 0.0, 0.0, -1.0] }, + '00200052': { + vr: 'UI', + Value: ['1.3.6.1.4.1.25403.345050719074.3824.20170126082902.5'], + }, + '00201040': { vr: 'LO' }, + '00201041': { vr: 'DS', Value: [342.0] }, + '00280002': { vr: 'US', Value: [1] }, + '00280004': { vr: 'CS', Value: ['MONOCHROME2'] }, + '00280010': { vr: 'US', Value: [512] }, + '00280011': { vr: 'US', Value: [512] }, + '00280030': { vr: 'DS', Value: [2.0, 2.0] }, + '00280100': { vr: 'US', Value: [16] }, + '00280101': { vr: 'US', Value: [16] }, + '00280102': { vr: 'US', Value: [15] }, + '00280103': { vr: 'US', Value: [1] }, + '00281050': { vr: 'DS', Value: [100.0] }, + '00281051': { vr: 'DS', Value: [230.0] }, + '00281052': { vr: 'DS', Value: [0.0] }, + '00281053': { vr: 'DS', Value: [1.0] }, + '00321033': { vr: 'LO', Value: ['OUTDFRAD'] }, + '00400002': { vr: 'DA', Value: ['20141125'] }, + '00400003': { vr: 'TM', Value: ['091000'] }, + '00400004': { vr: 'DA', Value: ['20141125'] }, + '00400005': { vr: 'TM', Value: ['094000.000'] }, + '00400244': { vr: 'DA', Value: ['20141125'] }, + '00400245': { vr: 'TM', Value: ['094528.000'] }, + '00400253': { vr: 'SH', Value: ['3708'] }, + '00400260': { + vr: 'SQ', + Value: [ + { + '00080100': { vr: 'SH', Value: ['6035'] }, + '00080102': { vr: 'SH', Value: ['CCG_CSTemp'] }, + '00080104': { vr: 'LO', Value: ['6035/DFCT2 CT 3-SITES W/OC'] }, + }, + ], + }, + '00402017': { vr: 'LO', Value: ['14159097'] }, + '7FE00010': { + vr: 'OW', + BulkDataURI: + 'http://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs/studies/1.3.6.1.4.1.25403.345050719074.3824.20170126082902.1/series/1.3.6.1.4.1.25403.345050719074.3824.20170126082902.2/instances/1.3.6.1.4.1.25403.345050719074.3824.20170126082902.3', + }, + }, +]; diff --git a/extensions/default/src/DataSources/DicomWebMinimalDataSource/index.ts b/extensions/default/src/DataSources/DicomWebMinimalDataSource/index.ts new file mode 100644 index 00000000000..4bad6c84af1 --- /dev/null +++ b/extensions/default/src/DataSources/DicomWebMinimalDataSource/index.ts @@ -0,0 +1,436 @@ +import { api } from 'dicomweb-client'; +import { IWebApiDataSource, utils, errorHandler, classes } from '@ohif/core'; + +import { + mapParams, + qidoSearch, + seriesInStudy, + processResults, + processSeriesResults, + listSeries, + deleteStudyMetadataPromise, + getImageId, + StaticWadoClient, + getDirectURL, + DicomWebConfig, + DicomDict, + denaturalizeDataset, + generateAuthorizationHeader, + retrieveMinimalSeriesMetadata, + retrieveSeriesMetadataAsync, +} from '../utils'; +import dcm4cheeReject from './dcm4cheeReject.js'; + +const ImplementationClassUID = '2.25.270695996825855179949881587723571202391.2.0.0'; +const ImplementationVersionName = 'OHIF-3.11.0'; +const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1'; + +const metadataProvider = classes.MetadataProvider; + +/** + * Creates a DICOM Web API based on the provided configuration. + * + * @param dicomWebConfig - Configuration for the DICOM Web API + * @returns DICOM Web API object + */ +function createDicomWebMinimalApi(dicomWebConfig: DicomWebConfig, servicesManager) { + const { userAuthenticationService } = servicesManager.services; + const dicomWebConfigCopy = JSON.parse(JSON.stringify(dicomWebConfig)); + let qidoConfig, + wadoConfig, + qidoDicomWebClient, + wadoDicomWebClient, + getAuthorizationHeader; + // Default to enabling bulk data retrieves, with no other customization as + // this is part of hte base standard. + dicomWebConfig.bulkDataURI ||= { enabled: true }; + + const implementation = { + initialize: ({ params, query }) => { + if (dicomWebConfig.onConfiguration && typeof dicomWebConfig.onConfiguration === 'function') { + dicomWebConfig = dicomWebConfig.onConfiguration(dicomWebConfig, { + params, + query, + }); + } + + getAuthorizationHeader = () => { + return generateAuthorizationHeader(userAuthenticationService); + } + + qidoConfig = { + url: dicomWebConfig.qidoRoot, + staticWado: dicomWebConfig.staticWado, + singlepart: dicomWebConfig.singlepart, + headers: userAuthenticationService.getAuthorizationHeader(), + errorInterceptor: errorHandler.getHTTPErrorHandler(), + supportsFuzzyMatching: dicomWebConfig.supportsFuzzyMatching, + }; + + wadoConfig = { + url: dicomWebConfig.wadoRoot, + staticWado: dicomWebConfig.staticWado, + singlepart: dicomWebConfig.singlepart, + headers: userAuthenticationService.getAuthorizationHeader(), + errorInterceptor: errorHandler.getHTTPErrorHandler(), + supportsFuzzyMatching: dicomWebConfig.supportsFuzzyMatching, + }; + + // TODO -> Two clients sucks, but its better than 1000. + // TODO -> We'll need to merge auth later. + qidoDicomWebClient = dicomWebConfig.staticWado + ? new StaticWadoClient(qidoConfig) + : new api.DICOMwebClient(qidoConfig); + + wadoDicomWebClient = dicomWebConfig.staticWado + ? new StaticWadoClient(wadoConfig) + : new api.DICOMwebClient(wadoConfig); + }, + query: { + studies: { + mapParams: mapParams.bind(), + search: async function (origParams) { + qidoDicomWebClient.headers = getAuthorizationHeader(); + const { studyInstanceUid, seriesInstanceUid, ...mappedParams } = + mapParams(origParams, { + supportsFuzzyMatching: dicomWebConfig.supportsFuzzyMatching, + supportsWildcard: dicomWebConfig.supportsWildcard, + }) || {}; + + const results = await qidoSearch(qidoDicomWebClient, undefined, undefined, mappedParams); + + return processResults(results); + }, + processResults: processResults.bind(), + }, + series: { + // mapParams: mapParams.bind(), + search: async function (studyInstanceUid) { + qidoDicomWebClient.headers = getAuthorizationHeader(); + const results = await seriesInStudy(qidoDicomWebClient, studyInstanceUid); + + return processSeriesResults(results); + }, + // processResults: processResults.bind(), + list: async (studyInstanceUid: string, filters) => { + if (!studyInstanceUid) { + throw new Error('Unable to query for Series List without studyInstanceUid'); + } + return listSeries(qidoDicomWebClient, studyInstanceUid, filters) + }, + }, + instances: { + search: (studyInstanceUid, queryParameters) => { + qidoDicomWebClient.headers = getAuthorizationHeader(); + return qidoSearch.call( + undefined, + qidoDicomWebClient, + studyInstanceUid, + null, + queryParameters + ); + }, + }, + }, + retrieve: { + /** + * Generates a URL that can be used for direct retrieve of the bulkdata + * + * @param {object} params + * @param {string} params.tag is the tag name of the URL to retrieve + * @param {object} params.instance is the instance object that the tag is in + * @param {string} params.defaultType is the mime type of the response + * @param {string} params.singlepart is the type of the part to retrieve + * @returns an absolute URL to the resource, if the absolute URL can be retrieved as singlepart, + * or is already retrieved, or a promise to a URL for such use if a BulkDataURI + */ + + getGetThumbnailSrc: function (instance, imageId) { + if (dicomWebConfig.thumbnailRendering === 'wadors') { + return function getThumbnailSrc(options) { + if (!imageId) { + return null; + } + if (!options?.getImageSrc) { + return null; + } + return options.getImageSrc(imageId); + }; + } + if (dicomWebConfig.thumbnailRendering === 'thumbnailDirect') { + return function getThumbnailSrc() { + return this.directURL({ + instance: instance, + defaultPath: '/thumbnail', + defaultType: 'image/jpeg', + singlepart: true, + tag: 'Absent', + }); + }.bind(this); + } + + if (dicomWebConfig.thumbnailRendering === 'thumbnail') { + return async function getThumbnailSrc() { + const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance; + const bulkDataURI = `${dicomWebConfig.wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/thumbnail?accept=image/jpeg`; + return URL.createObjectURL( + new Blob( + [ + await this.bulkDataURI({ + BulkDataURI: bulkDataURI.replace('wadors:', ''), + defaultType: 'image/jpeg', + mediaTypes: ['image/jpeg'], + thumbnail: true, + }), + ], + { type: 'image/jpeg' } + ) + ); + }.bind(this); + } + if (dicomWebConfig.thumbnailRendering === 'rendered') { + return async function getThumbnailSrc() { + const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance; + const bulkDataURI = `${dicomWebConfig.wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/rendered?accept=image/jpeg`; + return URL.createObjectURL( + new Blob( + [ + await this.bulkDataURI({ + BulkDataURI: bulkDataURI.replace('wadors:', ''), + defaultType: 'image/jpeg', + mediaTypes: ['image/jpeg'], + thumbnail: true, + }), + ], + { type: 'image/jpeg' } + ) + ); + }.bind(this); + } + }, + + directURL: params => { + return getDirectURL( + { + wadoRoot: dicomWebConfig.wadoRoot, + singlepart: dicomWebConfig.singlepart, + }, + params + ); + }, + /** + * Provide direct access to the dicom web client for certain use cases + * where the dicom web client is used by an external library such as the + * microscopy viewer. + * Note this instance only needs to support the wado queries, and may not + * support any QIDO or STOW operations. + */ + getWadoDicomWebClient: () => wadoDicomWebClient, + + bulkDataURI: async ({ StudyInstanceUID, BulkDataURI }) => { + qidoDicomWebClient.headers = getAuthorizationHeader(); + const options = { + multipart: false, + BulkDataURI, + StudyInstanceUID, + }; + return qidoDicomWebClient.retrieveBulkData(options).then(val => { + const ret = (val && val[0]) || undefined; + return ret; + }); + }, + series: { + metadata: async ({ + StudyInstanceUID, + filters, + sortCriteria, + sortFunction, + madeInClient = false, + returnPromises = false, + } = {}) => { + if (!StudyInstanceUID) { + throw new Error('Unable to query for SeriesMetadata without StudyInstanceUID'); + } + + if (dicomWebConfig.enableStudyLazyLoad) { + return implementation._retrieveSeriesMetadataAsync( + StudyInstanceUID, + filters, + sortCriteria, + sortFunction, + madeInClient, + returnPromises + ); + } + + return implementation._retrieveSeriesMetadataOptimizedSync( + StudyInstanceUID, + filters, + sortCriteria, + sortFunction, + madeInClient + ); + }, + }, + }, + + store: { + dicom: async (dataset, request, dicomDict) => { + wadoDicomWebClient.headers = getAuthorizationHeader(); + if (dataset instanceof ArrayBuffer) { + const options = { + datasets: [dataset], + request, + }; + await wadoDicomWebClient.storeInstances(options); + } else { + let effectiveDicomDict = dicomDict; + + if (!dicomDict) { + const meta = { + FileMetaInformationVersion: dataset._meta?.FileMetaInformationVersion?.Value, + MediaStorageSOPClassUID: dataset.SOPClassUID, + MediaStorageSOPInstanceUID: dataset.SOPInstanceUID, + TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN, + ImplementationClassUID, + ImplementationVersionName, + }; + + const denaturalized = denaturalizeDataset(meta); + const defaultDicomDict = new DicomDict(denaturalized); + defaultDicomDict.dict = denaturalizeDataset(dataset); + + effectiveDicomDict = defaultDicomDict; + } + + const part10Buffer = effectiveDicomDict.write(); + + const options = { + datasets: [part10Buffer], + request, + }; + + await wadoDicomWebClient.storeInstances(options); + } + }, + }, + + _retrieveSeriesMetadataOptimizedSync: async ( + StudyInstanceUID, + filters, + sortCriteria, + sortFunction, + madeInClient + ) => { + const getImageIdsForInstance = implementation.getImageIdsForInstance; + const retrieveDependencies = { + qidoDicomWebClient, + wadoDicomWebClient, + metadataProvider, + dicomWebConfig, + userAuthenticationService, + getImageIdsForInstance + } + return retrieveMinimalSeriesMetadata ( + StudyInstanceUID, + filters, + sortCriteria, + sortFunction, + madeInClient, + retrieveDependencies + ); + }, + + _retrieveSeriesMetadataAsync: async ( + StudyInstanceUID, + filters, + sortCriteria, + sortFunction, + madeInClient = false, + returnPromises = false + ) => { + const getImageIdsForInstance = implementation.getImageIdsForInstance; + const retrieveDependencies = { + qidoDicomWebClient, + wadoDicomWebClient, + metadataProvider, + dicomWebConfig, + userAuthenticationService, + getImageIdsForInstance + } + return retrieveSeriesMetadataAsync( + StudyInstanceUID, + filters, + sortCriteria, + sortFunction, + retrieveDependencies, + madeInClient, + returnPromises + ); + }, + deleteStudyMetadataPromise, + getImageIdsForDisplaySet(displaySet) { + const images = displaySet.images; + const imageIds = []; + + if (!images) { + return imageIds; + } + + displaySet.images.forEach(instance => { + const NumberOfFrames = instance.NumberOfFrames; + + if (NumberOfFrames > 1) { + for (let frame = 1; frame <= NumberOfFrames; frame++) { + const imageId = implementation.getImageIdsForInstance({ + instance, + frame, + }); + imageIds.push(imageId); + } + } else { + const imageId = implementation.getImageIdsForInstance({ + instance, + }); + imageIds.push(imageId); + } + }); + + return imageIds; + }, + getConfig() { + return dicomWebConfigCopy; + }, + getImageIdsForInstance({ instance, frame = undefined }) { + return getImageId( + instance, + frame, + dicomWebConfig, + ); + }, + getStudyInstanceUIDs({ params, query }) { + const paramsStudyInstanceUIDs = params.StudyInstanceUIDs || params.studyInstanceUIDs; + + const queryStudyInstanceUIDs = utils.splitComma( + query.getAll('StudyInstanceUIDs').concat(query.getAll('studyInstanceUIDs')) + ); + + const StudyInstanceUIDs = + (queryStudyInstanceUIDs.length && queryStudyInstanceUIDs) || paramsStudyInstanceUIDs; + const StudyInstanceUIDsAsArray = + StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs) + ? StudyInstanceUIDs + : [StudyInstanceUIDs]; + + return StudyInstanceUIDsAsArray; + }, + }; + + if (dicomWebConfig.supportsReject) { + implementation.reject = dcm4cheeReject(dicomWebConfig.wadoRoot, getAuthorizationHeader); + } + + return IWebApiDataSource.create(implementation); +} + +export { createDicomWebMinimalApi }; diff --git a/extensions/default/src/DicomWebProxyDataSource/index.ts b/extensions/default/src/DataSources/DicomWebProxyDataSource/index.ts similarity index 94% rename from extensions/default/src/DicomWebProxyDataSource/index.ts rename to extensions/default/src/DataSources/DicomWebProxyDataSource/index.ts index 8c83899aa01..ebfe608d7fd 100644 --- a/extensions/default/src/DicomWebProxyDataSource/index.ts +++ b/extensions/default/src/DataSources/DicomWebProxyDataSource/index.ts @@ -11,6 +11,7 @@ import { createDicomWebApi } from '../DicomWebDataSource/index'; */ function createDicomWebProxyApi(dicomWebProxyConfig, servicesManager: AppTypes.ServicesManager) { const { name } = dicomWebProxyConfig; + const dicomWebProxyConfigCopy = JSON.parse(JSON.stringify(dicomWebProxyConfig)); let dicomWebDelegate = undefined; const implementation = { @@ -69,6 +70,9 @@ function createDicomWebProxyApi(dicomWebProxyConfig, servicesManager: AppTypes.S studyInstanceUIDs = queryStudyInstanceUIDs.split(';'); return studyInstanceUIDs; }, + getConfig() { + return dicomWebProxyConfigCopy; + }, }; return IWebApiDataSource.create(implementation); } diff --git a/extensions/default/src/MergeDataSource/index.test.ts b/extensions/default/src/DataSources/MergeDataSource/index.test.ts similarity index 100% rename from extensions/default/src/MergeDataSource/index.test.ts rename to extensions/default/src/DataSources/MergeDataSource/index.test.ts diff --git a/extensions/default/src/MergeDataSource/index.ts b/extensions/default/src/DataSources/MergeDataSource/index.ts similarity index 98% rename from extensions/default/src/MergeDataSource/index.ts rename to extensions/default/src/DataSources/MergeDataSource/index.ts index 92f54151d9d..1ba599496b0 100644 --- a/extensions/default/src/MergeDataSource/index.ts +++ b/extensions/default/src/DataSources/MergeDataSource/index.ts @@ -173,6 +173,7 @@ function createMergeDataSourceApi( ) { const { seriesMerge } = mergeConfig; const { dataSourceNames, defaultDataSourceName } = seriesMerge; + const mergeConfigCopy = JSON.parse(JSON.stringify(mergeConfig)); const implementation = { initialize: (...args: unknown[]) => @@ -286,6 +287,9 @@ function createMergeDataSourceApi( dataSourceNames, defaultDataSourceName, }), + getConfig() { + return mergeConfigCopy; + }, }; return IWebApiDataSource.create(implementation); diff --git a/extensions/default/src/MergeDataSource/types.ts b/extensions/default/src/DataSources/MergeDataSource/types.ts similarity index 100% rename from extensions/default/src/MergeDataSource/types.ts rename to extensions/default/src/DataSources/MergeDataSource/types.ts diff --git a/extensions/default/src/DicomWebDataSource/utils/StaticWadoClient.ts b/extensions/default/src/DataSources/utils/StaticWadoClient.ts similarity index 94% rename from extensions/default/src/DicomWebDataSource/utils/StaticWadoClient.ts rename to extensions/default/src/DataSources/utils/StaticWadoClient.ts index a34cc9d9a94..67d1d9d32c4 100644 --- a/extensions/default/src/DicomWebDataSource/utils/StaticWadoClient.ts +++ b/extensions/default/src/DataSources/utils/StaticWadoClient.ts @@ -1,5 +1,6 @@ import { api } from 'dicomweb-client'; import fixMultipart from './fixMultipart'; +import { DicomWebClientOptionsInterface } from './Types'; const { DICOMwebClient } = api; @@ -122,11 +123,14 @@ export default class StaticWadoClient extends api.DICOMwebClient { return filtered; } - async searchForSeries(options) { + async searchForSeries(options: DicomWebClientOptionsInterface) { if (!this.staticWado) { return super.searchForSeries(options); } + // Linters will complain that awaiting on a non promise type is redundant, but if you skip this await + // images do not load. Linters might be getting confused with the dicomweb-client method + // signatures which call `_httpRequest` and return the promise. const searchResult = await super.searchForSeries(options); const { queryParams } = options; if (!queryParams) { @@ -146,6 +150,10 @@ export default class StaticWadoClient extends api.DICOMwebClient { return filtered; } + async searchForInstances(options?: DicomWebClientOptionsInterface) { + return super.searchForInstances(options); + } + /** * Compares values, matching any instance of desired to any instance of * actual by recursively go through the paired set of values. That is, diff --git a/extensions/default/src/DataSources/utils/Types.ts b/extensions/default/src/DataSources/utils/Types.ts new file mode 100644 index 00000000000..09cf556bb75 --- /dev/null +++ b/extensions/default/src/DataSources/utils/Types.ts @@ -0,0 +1,140 @@ +/** + * Expose all type definitions needed to track the object fields from the input metadata retrieved + * from the server. + */ + +///////////////////////////DICOM Types/////////////////////////////// + +export type RawFulfilledDicomInstance = PromiseFulfilledResult; +export type RawDicomInstance = any; +export type RawDicomInstances = RawDicomInstance[]; +export type SettledRawDicomInstances = RawDicomInstances[]; + +/** + * Series level metadata header type. Here, we collect all of the metadata necessary for describing + * an input study + */ +export type DicomSeriesHeaderMetaData = { + StudyInstanceUID: string, + StudyDescription: string, + SeriesInstanceUID: string, + SeriesDescription: string, + SeriesNumber: number, + SeriesTime: string, + SOPClassUID: string, + ProtocolName: string, + Modality: string, +} + +/** + * This type adds the fields we need to track an instance. Some of the data can be obtained from + * QIDO responses and other data such as ImagePositionPatient (IPP) comes from WADO responses. + */ +export interface DicomReferenceMetadata extends DicomSeriesHeaderMetaData { + SOPInstanceUID: string, + ImagePositionPatient?: number[], + BitsAllocated: number, + Rows: number, + Columns: number, + InstanceNumber: number, + NumberOfFrames?: number, + PixelData: { + BulkDataURI: string, + }, + imageId?: string, + url?: string, + wadoRoot?: string, + wadoUri?: string, +} +export type DicomStructure = DicomReferenceMetadata; +export type DicomStructureData = DicomStructure[]; + +export type DicomSeriesMetaData = Map; +export type DicomInstancesMetaData = Map; + +/** + * This type puts together all of the instance metadata and series metadata in one structure that + * mirrors the data structure expected by the Viewer. + */ +export type DicomStudyMetaData = { + seriesSummaryMetadata: DicomSeriesMetaData, + instancesPerSeries: DicomInstancesMetaData +} +export type DicomSeriesStructureData = DicomStructureData[]; + +///////////////////////////Retrieval Types////////////////////////////////// + +/** + * Deferred promise class type hadnling retrieval of data in a lazy context. + * + * If you use the constructor, the promise will be auto queued. Otherwise, you need to call start() + * to get the underlying promised queued in the background. + */ +export class DeferredPromise { + metadata = undefined; + processFunction = undefined; + internalPromise = undefined; + thenFunction = undefined; + rejectFunction = undefined; + + constructor(metadata, processFunction) { + this.setMetadata(metadata); + this.setProcessFunction(processFunction); + this.start(); + } + + setMetadata(metadata) { + this.metadata = metadata; + } + setProcessFunction(func) { + this.processFunction = func; + } + getPromise() { + return this.start(); + } + start() { + if (this.internalPromise) { + return this.internalPromise; + } + this.internalPromise = this.processFunction(); + // in case then and reject functions called before start + if (this.thenFunction) { + this.then(this.thenFunction); + this.thenFunction = undefined; + } + if (this.rejectFunction) { + this.reject(this.rejectFunction); + this.rejectFunction = undefined; + } + return this.internalPromise; + } + then(func) { + if (this.internalPromise) { + return this.internalPromise.then(func); + } else { + this.thenFunction = func; + } + } + reject(func) { + if (this.internalPromise) { + return this.internalPromise.reject(func); + } else { + this.rejectFunction = func; + } + } +} + +/** + * Interface to be used by retrieveStudyMetadata to annotate the expected result fields. + */ +export interface RetrieveStudyMetadataInterface { + preLoadData: Array, + promises: Array, +} + + +export interface DicomWebClientOptionsInterface { + studyInstanceUID?: string, + seriesInstanceUID?: string, + queryParams?: any, +} \ No newline at end of file diff --git a/extensions/default/src/DicomWebDataSource/utils/cleanDenaturalizedDataset.ts b/extensions/default/src/DataSources/utils/cleanDenaturalizedDataset.ts similarity index 100% rename from extensions/default/src/DicomWebDataSource/utils/cleanDenaturalizedDataset.ts rename to extensions/default/src/DataSources/utils/cleanDenaturalizedDataset.ts diff --git a/extensions/default/src/DataSources/utils/data.test.ts b/extensions/default/src/DataSources/utils/data.test.ts new file mode 100644 index 00000000000..223d20615d8 --- /dev/null +++ b/extensions/default/src/DataSources/utils/data.test.ts @@ -0,0 +1,1646 @@ +/** + * Repository of static data and variables to use as priori for testing. + */ +import { api } from 'dicomweb-client'; +import { dicomWebToDicomStructure } from './metadata/extractMetaData'; +import { errorHandler } from '@ohif/core'; +import { retrieveBulkData } from './wado/retrieveBulkData'; + +export const dicomWebConfig = { + friendlyName: 'AWS S3 Static wado server', + name: 'aws', + wadoUriRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + wadoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: true, + supportsWildcard: false, + staticWado: true, + 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', + transform: url => url.replace('/pixeldata.mp4', '/rendered'), + }, + omitQuotationForMultipartRequest: true, +}; +export const qidoConfig = { + url: dicomWebConfig.qidoRoot, + staticWado: dicomWebConfig.staticWado, + singlepart: dicomWebConfig.singlepart, + headers: { Accept: '' }, + errorInterceptor: errorHandler.getHTTPErrorHandler(), + supportsFuzzyMatching: dicomWebConfig.supportsFuzzyMatching, +}; +export const searchOptions = {}; +export const client = new api.DICOMwebClient(qidoConfig); +export const dicomInstances = [ + { + '00080013': { + vr: 'TM', + Value: ['110724'], + }, + '00080014': { + vr: 'UI', + Value: ['2.16.124.113543.6004.101.103'], + }, + '00080018': { + Value: ['1.3.6.1.4.1.9590.100.1.2.346385697213345623712464894863342033573'], + }, + '00080032': { + vr: 'TM', + Value: ['110724'], + }, + '00080033': { + vr: 'TM', + Value: ['110724'], + }, + '00081080': { + vr: 'LO', + Value: ['NA'], + }, + '00082111': { + vr: 'ST', + Value: ['NA'], + }, + '001021B0': { + vr: 'LT', + Value: ['Images from the 2002 AAPM task group report on display performan'], + }, + '00104000': { + vr: 'LT', + Value: ['NA'], + }, + '00181014': { + vr: 'TM', + Value: ['110724'], + }, + '00181023': { + vr: 'LO', + Value: ['Image exported from tShow application'], + }, + '00200013': { + vr: 'IS', + Value: [1], + }, + '00200060': { + vr: 'CS', + Value: ['NA'], + }, + '00201002': { + vr: 'IS', + Value: [1], + }, + '00280106': { + vr: 'US|SS', + InlineBinary: 'AAA=', + }, + '00280107': { + vr: 'US|SS', + InlineBinary: '/w8=', + }, + '7FE00010': { + vr: 'OB|OW', + BulkDataURI: + 'instances/1.3.6.1.4.1.9590.100.1.2.346385697213345623712464894863342033573/frames', + }, + '00083002': { + Value: ['1.2.840.10008.1.2.4.80'], + }, + '00090010': { + Value: ['dedupped'], + vr: 'CS', + }, + '00091010': { + Value: [ + '0f4205417d9d2658a75f28b85651939a9832e5649ef0475ad19a3d53956ac9a0', + '602b9c21e612ba224493bcbf1a867bde745371e7b60dd6a149fe6e8d7bba5af9', + 'dc77149ddb654f34e3fe66cb443495acb2b60fbc18ded168e31e0fc64147eeb9', + '36bf58abfb35d6a35cdf7f077d92d80980703c13e590e2074819c95428eac7cb', + ], + }, + '0020000E': { + Value: ['2.16.124.113543.6004.101.103.20021117.190619.1.001'], + }, + '00091011': { + Value: ['28b44927e9e3ec0003e5183bcb817c6ada9a84085b0e8ced4550a4c6be6cbaed'], + }, + '00091012': { + Value: ['instance'], + }, + '00100020': { + vr: 'LO', + Value: ['TG18-2002'], + }, + '00100010': { + vr: 'PN', + Value: [ + { + Alphabetic: 'AAPM^Test^Patterns', + }, + ], + }, + '00100030': { + vr: 'DA', + Value: ['20020704'], + }, + '00100040': { + vr: 'CS', + Value: ['O'], + }, + '00081030': { + vr: 'LO', + Value: ['Multi Purpose 1K'], + }, + '00080050': { + vr: 'SH', + Value: ['20022002'], + }, + '0020000D': { + vr: 'UI', + Value: ['2.16.124.113543.6004.101.103.20021117.190619.1'], + }, + '00080020': { + vr: 'DA', + Value: ['20180724'], + }, + '00080030': { + vr: 'TM', + Value: ['190619'], + }, + '00200010': { + vr: 'SH', + Value: ['1K-MULTI'], + }, + '0008103E': { + vr: 'LO', + Value: ['TG18-OIQ'], + }, + '00200011': { + vr: 'IS', + Value: [1], + }, + '00080060': { + vr: 'CS', + Value: ['OT'], + }, + '00080021': { + vr: 'DA', + Value: ['20180724'], + }, + '00080031': { + vr: 'TM', + Value: ['110724'], + }, + '00080090': { + vr: 'PN', + Value: [ + { + Alphabetic: 'AAPM', + }, + ], + }, + '00180015': { + vr: 'CS', + Value: ['NA'], + }, + '00181030': { + vr: 'LO', + Value: ['Display Quality Test Protocol'], + }, + '00080008': { + vr: 'CS', + Value: ['ORIGINAL'], + }, + '00080012': { + vr: 'DA', + Value: ['20180724'], + }, + '00080016': { + vr: 'UI', + Value: ['1.2.840.10008.5.1.4.1.1.7'], + }, + '00080064': { + vr: 'CS', + Value: ['WSD'], + }, + '00080023': { + vr: 'DA', + Value: ['20180724'], + }, + '00080022': { + vr: 'DA', + Value: ['20180724'], + }, + '00181012': { + vr: 'DA', + Value: ['20180724'], + }, + '00181016': { + vr: 'LO', + Value: ['Duke University Health System'], + }, + '00181018': { + vr: 'LO', + Value: ['MATLAB'], + }, + '00280002': { + vr: 'US', + Value: [1], + }, + '00280004': { + vr: 'CS', + Value: ['MONOCHROME2'], + }, + '00280010': { + vr: 'US', + Value: [1024], + }, + '00280011': { + vr: 'US', + Value: [1024], + }, + '00280100': { + vr: 'US', + Value: [16], + }, + '00280101': { + vr: 'US', + Value: [16], + }, + '00280102': { + vr: 'US', + Value: [15], + }, + '00280103': { + vr: 'US', + Value: [0], + }, + '00200020': { + vr: 'CS', + }, + }, + { + '00080013': { + vr: 'TM', + Value: ['140724'], + }, + '00080014': { + vr: 'UI', + Value: ['2.16.124.113543.6004.101.103'], + }, + '00080018': { + Value: ['1.3.6.1.4.1.9590.100.1.2.294498401812162035928148179312426786986'], + }, + '00080032': { + vr: 'TM', + Value: ['140724'], + }, + '00080033': { + vr: 'TM', + Value: ['140724'], + }, + '00081080': { + vr: 'LO', + Value: ['NA'], + }, + '00082111': { + vr: 'ST', + Value: ['NA'], + }, + '001021B0': { + vr: 'LT', + Value: ['Images from the 2002 AAPM task group report on display performan'], + }, + '00104000': { + vr: 'LT', + Value: ['NA'], + }, + '00181014': { + vr: 'TM', + Value: ['140724'], + }, + '00181023': { + vr: 'LO', + Value: ['Image exported from tShow application'], + }, + '00200013': { + vr: 'IS', + Value: [1], + }, + '00200060': { + vr: 'CS', + Value: ['NA'], + }, + '00201002': { + vr: 'IS', + Value: [1], + }, + '00280106': { + vr: 'US|SS', + InlineBinary: 'AAA=', + }, + '00280107': { + vr: 'US|SS', + InlineBinary: '/w8=', + }, + '7FE00010': { + vr: 'OB|OW', + BulkDataURI: + 'instances/1.3.6.1.4.1.9590.100.1.2.294498401812162035928148179312426786986/frames', + }, + '00083002': { + Value: ['1.2.840.10008.1.2.4.80'], + }, + '00090010': { + Value: ['dedupped'], + vr: 'CS', + }, + '00091010': { + Value: [ + '0f4205417d9d2658a75f28b85651939a9832e5649ef0475ad19a3d53956ac9a0', + '602b9c21e612ba224493bcbf1a867bde745371e7b60dd6a149fe6e8d7bba5af9', + '07583ddd16e12b13e95a3a4a0bdd7b7890a4aad251e94cb91323e5286187b915', + '4d311847ac3f203f23fd3db59316f3508c904e5d093bab489ca1662f8d67b56b', + ], + }, + '0020000E': { + Value: ['2.16.124.113543.6004.101.103.20021117.190619.1.001'], + }, + '00091011': { + Value: ['ce8828ba234b481d0d0c8c62898b6779fdc0c7bac7e9ee691e27806664108522'], + }, + '00091012': { + Value: ['instance'], + }, + '00100020': { + vr: 'LO', + Value: ['TG18-2002'], + }, + '00100010': { + vr: 'PN', + Value: [ + { + Alphabetic: 'AAPM^Test^Patterns', + }, + ], + }, + '00100030': { + vr: 'DA', + Value: ['20020704'], + }, + '00100040': { + vr: 'CS', + Value: ['O'], + }, + '00081030': { + vr: 'LO', + Value: ['Multi Purpose 1K'], + }, + '00080050': { + vr: 'SH', + Value: ['20022002'], + }, + '0020000D': { + vr: 'UI', + Value: ['2.16.124.113543.6004.101.103.20021117.190619.1'], + }, + '00080020': { + vr: 'DA', + Value: ['20180724'], + }, + '00080030': { + vr: 'TM', + Value: ['190619'], + }, + '00200010': { + vr: 'SH', + Value: ['1K-MULTI'], + }, + '0008103E': { + vr: 'LO', + Value: ['TG18-OIQ'], + }, + '00200011': { + vr: 'IS', + Value: [1], + }, + '00080060': { + vr: 'CS', + Value: ['OT'], + }, + '00080021': { + vr: 'DA', + Value: ['20180724'], + }, + '00080031': { + vr: 'TM', + Value: ['140724'], + }, + '00080090': { + vr: 'PN', + Value: [ + { + Alphabetic: 'AAPM', + }, + ], + }, + '00180015': { + vr: 'CS', + Value: ['NA'], + }, + '00181030': { + vr: 'LO', + Value: ['Display Quality Test Protocol'], + }, + '00080008': { + vr: 'CS', + Value: ['ORIGINAL'], + }, + '00080012': { + vr: 'DA', + Value: ['20180724'], + }, + '00080016': { + vr: 'UI', + Value: ['1.2.840.10008.5.1.4.1.1.7'], + }, + '00080064': { + vr: 'CS', + Value: ['WSD'], + }, + '00080023': { + vr: 'DA', + Value: ['20180724'], + }, + '00080022': { + vr: 'DA', + Value: ['20180724'], + }, + '00181012': { + vr: 'DA', + Value: ['20180724'], + }, + '00181016': { + vr: 'LO', + Value: ['Duke University Health System'], + }, + '00181018': { + vr: 'LO', + Value: ['MATLAB'], + }, + '00280002': { + vr: 'US', + Value: [1], + }, + '00280004': { + vr: 'CS', + Value: ['MONOCHROME2'], + }, + '00280010': { + vr: 'US', + Value: [1024], + }, + '00280011': { + vr: 'US', + Value: [1280], + }, + '00280100': { + vr: 'US', + Value: [16], + }, + '00280101': { + vr: 'US', + Value: [16], + }, + '00280102': { + vr: 'US', + Value: [15], + }, + '00280103': { + vr: 'US', + Value: [0], + }, + '00200020': { + vr: 'CS', + }, + }, + { + '00080013': { + vr: 'TM', + Value: ['160738'], + }, + '00080014': { + vr: 'UI', + Value: ['2.16.124.113543.6004.101.103'], + }, + '00080018': { + Value: ['1.3.6.1.4.1.9590.100.1.2.304484424913537637032577967974004238574'], + }, + '00080032': { + vr: 'TM', + Value: ['160738'], + }, + '00080033': { + vr: 'TM', + Value: ['160738'], + }, + '00081080': { + vr: 'LO', + Value: ['NA'], + }, + '00082111': { + vr: 'ST', + Value: ['NA'], + }, + '001021B0': { + vr: 'LT', + Value: ['Images from the 2002 AAPM task group report on display performan'], + }, + '00104000': { + vr: 'LT', + Value: ['NA'], + }, + '00181014': { + vr: 'TM', + Value: ['160738'], + }, + '00181023': { + vr: 'LO', + Value: ['Image exported from tShow application'], + }, + '00200013': { + vr: 'IS', + Value: [1], + }, + '00200060': { + vr: 'CS', + Value: ['NA'], + }, + '00201002': { + vr: 'IS', + Value: [1], + }, + '00280106': { + vr: 'US|SS', + InlineBinary: 'AAA=', + }, + '00280107': { + vr: 'US|SS', + InlineBinary: '/w8=', + }, + '7FE00010': { + vr: 'OB|OW', + BulkDataURI: + 'instances/1.3.6.1.4.1.9590.100.1.2.304484424913537637032577967974004238574/frames', + }, + '00083002': { + Value: ['1.2.840.10008.1.2.4.80'], + }, + '00090010': { + Value: ['dedupped'], + vr: 'CS', + }, + '00091010': { + Value: [ + '0f4205417d9d2658a75f28b85651939a9832e5649ef0475ad19a3d53956ac9a0', + '602b9c21e612ba224493bcbf1a867bde745371e7b60dd6a149fe6e8d7bba5af9', + '93df3408efa1dc2e9631eb030a9ac802b0844b3c03606ecc597dcb12e0c6e812', + '5ae910ca86c68a269dd52aeae2d5faa167bc76b6474c463ebd8757c6a8f3758a', + ], + }, + '0020000E': { + Value: ['2.16.124.113543.6004.101.103.20021117.190619.1.001'], + }, + '00091011': { + Value: ['988f02f735e0fc7372ff4e7373877689529135e238712ecc664961d0d3f20c27'], + }, + '00091012': { + Value: ['instance'], + }, + '00100020': { + vr: 'LO', + Value: ['TG18-2002'], + }, + '00100010': { + vr: 'PN', + Value: [ + { + Alphabetic: 'AAPM^Test^Patterns', + }, + ], + }, + '00100030': { + vr: 'DA', + Value: ['20020704'], + }, + '00100040': { + vr: 'CS', + Value: ['O'], + }, + '00081030': { + vr: 'LO', + Value: ['Multi Purpose 1K'], + }, + '00080050': { + vr: 'SH', + Value: ['20022002'], + }, + '0020000D': { + vr: 'UI', + Value: ['2.16.124.113543.6004.101.103.20021117.190619.1'], + }, + '00080020': { + vr: 'DA', + Value: ['20180724'], + }, + '00080030': { + vr: 'TM', + Value: ['190619'], + }, + '00200010': { + vr: 'SH', + Value: ['1K-MULTI'], + }, + '0008103E': { + vr: 'LO', + Value: ['TG18-OIQ'], + }, + '00200011': { + vr: 'IS', + Value: [1], + }, + '00080060': { + vr: 'CS', + Value: ['OT'], + }, + '00080021': { + vr: 'DA', + Value: ['20180724'], + }, + '00080031': { + vr: 'TM', + Value: ['160738'], + }, + '00080090': { + vr: 'PN', + Value: [ + { + Alphabetic: 'AAPM', + }, + ], + }, + '00180015': { + vr: 'CS', + Value: ['NA'], + }, + '00181030': { + vr: 'LO', + Value: ['Display Quality Test Protocol'], + }, + '00080008': { + vr: 'CS', + Value: ['ORIGINAL'], + }, + '00080012': { + vr: 'DA', + Value: ['20180724'], + }, + '00080016': { + vr: 'UI', + Value: ['1.2.840.10008.5.1.4.1.1.7'], + }, + '00080064': { + vr: 'CS', + Value: ['WSD'], + }, + '00080023': { + vr: 'DA', + Value: ['20180724'], + }, + '00080022': { + vr: 'DA', + Value: ['20180724'], + }, + '00181012': { + vr: 'DA', + Value: ['20180724'], + }, + '00181016': { + vr: 'LO', + Value: ['Duke University Health System'], + }, + '00181018': { + vr: 'LO', + Value: ['MATLAB'], + }, + '00280002': { + vr: 'US', + Value: [1], + }, + '00280004': { + vr: 'CS', + Value: ['MONOCHROME2'], + }, + '00280010': { + vr: 'US', + Value: [1280], + }, + '00280011': { + vr: 'US', + Value: [1024], + }, + '00280100': { + vr: 'US', + Value: [16], + }, + '00280101': { + vr: 'US', + Value: [16], + }, + '00280102': { + vr: 'US', + Value: [15], + }, + '00280103': { + vr: 'US', + Value: [0], + }, + '00200020': { + vr: 'CS', + }, + }, +]; +export const naturalizedInstances = dicomWebToDicomStructure(dicomInstances); +export const expectedNaturalizedInstances = [ + { + _vrMap: { + SmallestImagePixelValue: 'US|SS', + LargestImagePixelValue: 'US|SS', + PixelData: 'OB|OW', + }, + InstanceCreationTime: '110724', + InstanceCreatorUID: '2.16.124.113543.6004.101.103', + SOPInstanceUID: '1.3.6.1.4.1.9590.100.1.2.346385697213345623712464894863342033573', + AcquisitionTime: '110724', + ContentTime: '110724', + AdmittingDiagnosesDescription: 'NA', + DerivationDescription: 'NA', + AdditionalPatientHistory: 'Images from the 2002 AAPM task group report on display performan', + PatientComments: 'NA', + TimeOfSecondaryCapture: '110724', + DigitalImageFormatAcquired: 'Image exported from tShow application', + InstanceNumber: 1, + Laterality: 'NA', + ImagesInAcquisition: 1, + SmallestImagePixelValue: { InlineBinary: 'AAA=' }, + LargestImagePixelValue: { InlineBinary: '/w8=' }, + PixelData: { + BulkDataURI: + 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb/studies/2.16.124.113543.6004.101.103.20021117.190619.1/series/2.16.124.113543.6004.101.103.20021117.190619.1.001/instances/1.3.6.1.4.1.9590.100.1.2.346385697213345623712464894863342033573/frames', + }, + AvailableTransferSyntaxUID: '1.2.840.10008.1.2.4.80', + '00090010': 'dedupped', + '00091010': [ + '0f4205417d9d2658a75f28b85651939a9832e5649ef0475ad19a3d53956ac9a0', + '602b9c21e612ba224493bcbf1a867bde745371e7b60dd6a149fe6e8d7bba5af9', + 'dc77149ddb654f34e3fe66cb443495acb2b60fbc18ded168e31e0fc64147eeb9', + '36bf58abfb35d6a35cdf7f077d92d80980703c13e590e2074819c95428eac7cb', + ], + SeriesInstanceUID: '2.16.124.113543.6004.101.103.20021117.190619.1.001', + '00091011': '28b44927e9e3ec0003e5183bcb817c6ada9a84085b0e8ced4550a4c6be6cbaed', + '00091012': 'instance', + PatientID: 'TG18-2002', + PatientName: [{ Alphabetic: 'AAPM^Test^Patterns' }], + PatientBirthDate: '20020704', + PatientSex: 'O', + StudyDescription: 'Multi Purpose 1K', + AccessionNumber: '20022002', + StudyInstanceUID: '2.16.124.113543.6004.101.103.20021117.190619.1', + StudyDate: '20180724', + StudyTime: '190619', + StudyID: '1K-MULTI', + SeriesDescription: 'TG18-OIQ', + SeriesNumber: 1, + Modality: 'OT', + SeriesDate: '20180724', + SeriesTime: '110724', + ReferringPhysicianName: [{ Alphabetic: 'AAPM' }], + BodyPartExamined: 'NA', + ProtocolName: 'Display Quality Test Protocol', + ImageType: 'ORIGINAL', + InstanceCreationDate: '20180724', + SOPClassUID: '1.2.840.10008.5.1.4.1.1.7', + ConversionType: 'WSD', + ContentDate: '20180724', + AcquisitionDate: '20180724', + DateOfSecondaryCapture: '20180724', + SecondaryCaptureDeviceManufacturer: 'Duke University Health System', + SecondaryCaptureDeviceManufacturerModelName: 'MATLAB', + SamplesPerPixel: 1, + PhotometricInterpretation: 'MONOCHROME2', + Rows: 1024, + Columns: 1024, + BitsAllocated: 16, + BitsStored: 16, + HighBit: 15, + PixelRepresentation: 0, + PatientOrientation: null, + }, + { + _vrMap: { + SmallestImagePixelValue: 'US|SS', + LargestImagePixelValue: 'US|SS', + PixelData: 'OB|OW', + }, + InstanceCreationTime: '140724', + InstanceCreatorUID: '2.16.124.113543.6004.101.103', + SOPInstanceUID: '1.3.6.1.4.1.9590.100.1.2.294498401812162035928148179312426786986', + AcquisitionTime: '140724', + ContentTime: '140724', + AdmittingDiagnosesDescription: 'NA', + DerivationDescription: 'NA', + AdditionalPatientHistory: 'Images from the 2002 AAPM task group report on display performan', + PatientComments: 'NA', + TimeOfSecondaryCapture: '140724', + DigitalImageFormatAcquired: 'Image exported from tShow application', + InstanceNumber: 1, + Laterality: 'NA', + ImagesInAcquisition: 1, + SmallestImagePixelValue: { InlineBinary: 'AAA=' }, + LargestImagePixelValue: { InlineBinary: '/w8=' }, + PixelData: { + BulkDataURI: + 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb/studies/2.16.124.113543.6004.101.103.20021117.190619.1/series/2.16.124.113543.6004.101.103.20021117.190619.1.001/instances/1.3.6.1.4.1.9590.100.1.2.294498401812162035928148179312426786986/frames', + }, + AvailableTransferSyntaxUID: '1.2.840.10008.1.2.4.80', + '00090010': 'dedupped', + '00091010': [ + '0f4205417d9d2658a75f28b85651939a9832e5649ef0475ad19a3d53956ac9a0', + '602b9c21e612ba224493bcbf1a867bde745371e7b60dd6a149fe6e8d7bba5af9', + '07583ddd16e12b13e95a3a4a0bdd7b7890a4aad251e94cb91323e5286187b915', + '4d311847ac3f203f23fd3db59316f3508c904e5d093bab489ca1662f8d67b56b', + ], + SeriesInstanceUID: '2.16.124.113543.6004.101.103.20021117.190619.1.001', + '00091011': 'ce8828ba234b481d0d0c8c62898b6779fdc0c7bac7e9ee691e27806664108522', + '00091012': 'instance', + PatientID: 'TG18-2002', + PatientName: [{ Alphabetic: 'AAPM^Test^Patterns' }], + PatientBirthDate: '20020704', + PatientSex: 'O', + StudyDescription: 'Multi Purpose 1K', + AccessionNumber: '20022002', + StudyInstanceUID: '2.16.124.113543.6004.101.103.20021117.190619.1', + StudyDate: '20180724', + StudyTime: '190619', + StudyID: '1K-MULTI', + SeriesDescription: 'TG18-OIQ', + SeriesNumber: 1, + Modality: 'OT', + SeriesDate: '20180724', + SeriesTime: '140724', + ReferringPhysicianName: [{ Alphabetic: 'AAPM' }], + BodyPartExamined: 'NA', + ProtocolName: 'Display Quality Test Protocol', + ImageType: 'ORIGINAL', + InstanceCreationDate: '20180724', + SOPClassUID: '1.2.840.10008.5.1.4.1.1.7', + ConversionType: 'WSD', + ContentDate: '20180724', + AcquisitionDate: '20180724', + DateOfSecondaryCapture: '20180724', + SecondaryCaptureDeviceManufacturer: 'Duke University Health System', + SecondaryCaptureDeviceManufacturerModelName: 'MATLAB', + SamplesPerPixel: 1, + PhotometricInterpretation: 'MONOCHROME2', + Rows: 1024, + Columns: 1280, + BitsAllocated: 16, + BitsStored: 16, + HighBit: 15, + PixelRepresentation: 0, + PatientOrientation: null, + }, + { + _vrMap: { + SmallestImagePixelValue: 'US|SS', + LargestImagePixelValue: 'US|SS', + PixelData: 'OB|OW', + }, + InstanceCreationTime: '160738', + InstanceCreatorUID: '2.16.124.113543.6004.101.103', + SOPInstanceUID: '1.3.6.1.4.1.9590.100.1.2.304484424913537637032577967974004238574', + AcquisitionTime: '160738', + ContentTime: '160738', + AdmittingDiagnosesDescription: 'NA', + DerivationDescription: 'NA', + AdditionalPatientHistory: 'Images from the 2002 AAPM task group report on display performan', + PatientComments: 'NA', + TimeOfSecondaryCapture: '160738', + DigitalImageFormatAcquired: 'Image exported from tShow application', + InstanceNumber: 1, + Laterality: 'NA', + ImagesInAcquisition: 1, + SmallestImagePixelValue: { InlineBinary: 'AAA=' }, + LargestImagePixelValue: { InlineBinary: '/w8=' }, + PixelData: { + BulkDataURI: + 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb/studies/2.16.124.113543.6004.101.103.20021117.190619.1/series/2.16.124.113543.6004.101.103.20021117.190619.1.001/instances/1.3.6.1.4.1.9590.100.1.2.304484424913537637032577967974004238574/frames', + }, + AvailableTransferSyntaxUID: '1.2.840.10008.1.2.4.80', + '00090010': 'dedupped', + '00091010': [ + '0f4205417d9d2658a75f28b85651939a9832e5649ef0475ad19a3d53956ac9a0', + '602b9c21e612ba224493bcbf1a867bde745371e7b60dd6a149fe6e8d7bba5af9', + '93df3408efa1dc2e9631eb030a9ac802b0844b3c03606ecc597dcb12e0c6e812', + '5ae910ca86c68a269dd52aeae2d5faa167bc76b6474c463ebd8757c6a8f3758a', + ], + SeriesInstanceUID: '2.16.124.113543.6004.101.103.20021117.190619.1.001', + '00091011': '988f02f735e0fc7372ff4e7373877689529135e238712ecc664961d0d3f20c27', + '00091012': 'instance', + PatientID: 'TG18-2002', + PatientName: [{ Alphabetic: 'AAPM^Test^Patterns' }], + PatientBirthDate: '20020704', + PatientSex: 'O', + StudyDescription: 'Multi Purpose 1K', + AccessionNumber: '20022002', + StudyInstanceUID: '2.16.124.113543.6004.101.103.20021117.190619.1', + StudyDate: '20180724', + StudyTime: '190619', + StudyID: '1K-MULTI', + SeriesDescription: 'TG18-OIQ', + SeriesNumber: 1, + Modality: 'OT', + SeriesDate: '20180724', + SeriesTime: '160738', + ReferringPhysicianName: [{ Alphabetic: 'AAPM' }], + BodyPartExamined: 'NA', + ProtocolName: 'Display Quality Test Protocol', + ImageType: 'ORIGINAL', + InstanceCreationDate: '20180724', + SOPClassUID: '1.2.840.10008.5.1.4.1.1.7', + ConversionType: 'WSD', + ContentDate: '20180724', + AcquisitionDate: '20180724', + DateOfSecondaryCapture: '20180724', + SecondaryCaptureDeviceManufacturer: 'Duke University Health System', + SecondaryCaptureDeviceManufacturerModelName: 'MATLAB', + SamplesPerPixel: 1, + PhotometricInterpretation: 'MONOCHROME2', + Rows: 1280, + Columns: 1024, + BitsAllocated: 16, + BitsStored: 16, + HighBit: 15, + PixelRepresentation: 0, + PatientOrientation: null, + }, +]; +export const bulkDataURIExample = + 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb/studies/2.16.124.113543.6004.101.103.20021117.190619.1/series/2.16.124.113543.6004.101.103.20021117.190619.1.001/instances/1.3.6.1.4.1.9590.100.1.2.304484424913537637032577967974004238574/frames'; +export const expectedStudyMetadata = { + seriesSummaryMetadata: { + '2.16.124.113543.6004.101.103.20021117.190619.1.001': { + StudyInstanceUID: '2.16.124.113543.6004.101.103.20021117.190619.1', + StudyDescription: 'Multi Purpose 1K', + SeriesInstanceUID: '2.16.124.113543.6004.101.103.20021117.190619.1.001', + SeriesDescription: 'TG18-OIQ', + SeriesNumber: 1, + SeriesTime: '110724', + SOPClassUID: '1.2.840.10008.5.1.4.1.1.7', + ProtocolName: 'Display Quality Test Protocol', + Modality: 'OT', + }, + }, + instancesPerSeries: { + '2.16.124.113543.6004.101.103.20021117.190619.1.001': [ + { + _vrMap: { + SmallestImagePixelValue: 'US|SS', + LargestImagePixelValue: 'US|SS', + PixelData: 'OB|OW', + }, + InstanceCreationTime: '110724', + InstanceCreatorUID: '2.16.124.113543.6004.101.103', + SOPInstanceUID: '1.3.6.1.4.1.9590.100.1.2.346385697213345623712464894863342033573', + AcquisitionTime: '110724', + ContentTime: '110724', + AdmittingDiagnosesDescription: 'NA', + DerivationDescription: 'NA', + AdditionalPatientHistory: + 'Images from the 2002 AAPM task group report on display performan', + PatientComments: 'NA', + TimeOfSecondaryCapture: '110724', + DigitalImageFormatAcquired: 'Image exported from tShow application', + InstanceNumber: 1, + Laterality: 'NA', + ImagesInAcquisition: 1, + SmallestImagePixelValue: { InlineBinary: 'AAA=' }, + LargestImagePixelValue: { InlineBinary: '/w8=' }, + PixelData: { + BulkDataURI: + 'instances/1.3.6.1.4.1.9590.100.1.2.346385697213345623712464894863342033573/frames', + }, + AvailableTransferSyntaxUID: '1.2.840.10008.1.2.4.80', + '00090010': 'dedupped', + '00091010': [ + '0f4205417d9d2658a75f28b85651939a9832e5649ef0475ad19a3d53956ac9a0', + '602b9c21e612ba224493bcbf1a867bde745371e7b60dd6a149fe6e8d7bba5af9', + 'dc77149ddb654f34e3fe66cb443495acb2b60fbc18ded168e31e0fc64147eeb9', + '36bf58abfb35d6a35cdf7f077d92d80980703c13e590e2074819c95428eac7cb', + ], + SeriesInstanceUID: '2.16.124.113543.6004.101.103.20021117.190619.1.001', + '00091011': '28b44927e9e3ec0003e5183bcb817c6ada9a84085b0e8ced4550a4c6be6cbaed', + '00091012': 'instance', + PatientID: 'TG18-2002', + PatientName: [{ Alphabetic: 'AAPM^Test^Patterns' }], + PatientBirthDate: '20020704', + PatientSex: 'O', + StudyDescription: 'Multi Purpose 1K', + AccessionNumber: '20022002', + StudyInstanceUID: '2.16.124.113543.6004.101.103.20021117.190619.1', + StudyDate: '20180724', + StudyTime: '190619', + StudyID: '1K-MULTI', + SeriesDescription: 'TG18-OIQ', + SeriesNumber: 1, + Modality: 'OT', + SeriesDate: '20180724', + SeriesTime: '110724', + ReferringPhysicianName: [{ Alphabetic: 'AAPM' }], + BodyPartExamined: 'NA', + ProtocolName: 'Display Quality Test Protocol', + ImageType: 'ORIGINAL', + InstanceCreationDate: '20180724', + SOPClassUID: '1.2.840.10008.5.1.4.1.1.7', + ConversionType: 'WSD', + ContentDate: '20180724', + AcquisitionDate: '20180724', + DateOfSecondaryCapture: '20180724', + SecondaryCaptureDeviceManufacturer: 'Duke University Health System', + SecondaryCaptureDeviceManufacturerModelName: 'MATLAB', + SamplesPerPixel: 1, + PhotometricInterpretation: 'MONOCHROME2', + Rows: 1024, + Columns: 1024, + BitsAllocated: 16, + BitsStored: 16, + HighBit: 15, + PixelRepresentation: 0, + PatientOrientation: null, + imageId: + 'wadors:https://d14fa38qiwhyfd.cloudfront.net/dicomweb/studies/2.16.124.113543.6004.101.103.20021117.190619.1/series/2.16.124.113543.6004.101.103.20021117.190619.1.001/instances/1.3.6.1.4.1.9590.100.1.2.346385697213345623712464894863342033573/frames/1', + wadoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + }, + { + _vrMap: { + SmallestImagePixelValue: 'US|SS', + LargestImagePixelValue: 'US|SS', + PixelData: 'OB|OW', + }, + InstanceCreationTime: '140724', + InstanceCreatorUID: '2.16.124.113543.6004.101.103', + SOPInstanceUID: '1.3.6.1.4.1.9590.100.1.2.294498401812162035928148179312426786986', + AcquisitionTime: '140724', + ContentTime: '140724', + AdmittingDiagnosesDescription: 'NA', + DerivationDescription: 'NA', + AdditionalPatientHistory: + 'Images from the 2002 AAPM task group report on display performan', + PatientComments: 'NA', + TimeOfSecondaryCapture: '140724', + DigitalImageFormatAcquired: 'Image exported from tShow application', + InstanceNumber: 1, + Laterality: 'NA', + ImagesInAcquisition: 1, + SmallestImagePixelValue: { InlineBinary: 'AAA=' }, + LargestImagePixelValue: { InlineBinary: '/w8=' }, + PixelData: { + BulkDataURI: + 'instances/1.3.6.1.4.1.9590.100.1.2.294498401812162035928148179312426786986/frames', + }, + AvailableTransferSyntaxUID: '1.2.840.10008.1.2.4.80', + '00090010': 'dedupped', + '00091010': [ + '0f4205417d9d2658a75f28b85651939a9832e5649ef0475ad19a3d53956ac9a0', + '602b9c21e612ba224493bcbf1a867bde745371e7b60dd6a149fe6e8d7bba5af9', + '07583ddd16e12b13e95a3a4a0bdd7b7890a4aad251e94cb91323e5286187b915', + '4d311847ac3f203f23fd3db59316f3508c904e5d093bab489ca1662f8d67b56b', + ], + SeriesInstanceUID: '2.16.124.113543.6004.101.103.20021117.190619.1.001', + '00091011': 'ce8828ba234b481d0d0c8c62898b6779fdc0c7bac7e9ee691e27806664108522', + '00091012': 'instance', + PatientID: 'TG18-2002', + PatientName: [{ Alphabetic: 'AAPM^Test^Patterns' }], + PatientBirthDate: '20020704', + PatientSex: 'O', + StudyDescription: 'Multi Purpose 1K', + AccessionNumber: '20022002', + StudyInstanceUID: '2.16.124.113543.6004.101.103.20021117.190619.1', + StudyDate: '20180724', + StudyTime: '190619', + StudyID: '1K-MULTI', + SeriesDescription: 'TG18-OIQ', + SeriesNumber: 1, + Modality: 'OT', + SeriesDate: '20180724', + SeriesTime: '140724', + ReferringPhysicianName: [{ Alphabetic: 'AAPM' }], + BodyPartExamined: 'NA', + ProtocolName: 'Display Quality Test Protocol', + ImageType: 'ORIGINAL', + InstanceCreationDate: '20180724', + SOPClassUID: '1.2.840.10008.5.1.4.1.1.7', + ConversionType: 'WSD', + ContentDate: '20180724', + AcquisitionDate: '20180724', + DateOfSecondaryCapture: '20180724', + SecondaryCaptureDeviceManufacturer: 'Duke University Health System', + SecondaryCaptureDeviceManufacturerModelName: 'MATLAB', + SamplesPerPixel: 1, + PhotometricInterpretation: 'MONOCHROME2', + Rows: 1024, + Columns: 1280, + BitsAllocated: 16, + BitsStored: 16, + HighBit: 15, + PixelRepresentation: 0, + PatientOrientation: null, + imageId: + 'wadors:https://d14fa38qiwhyfd.cloudfront.net/dicomweb/studies/2.16.124.113543.6004.101.103.20021117.190619.1/series/2.16.124.113543.6004.101.103.20021117.190619.1.001/instances/1.3.6.1.4.1.9590.100.1.2.294498401812162035928148179312426786986/frames/1', + wadoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + }, + { + _vrMap: { + SmallestImagePixelValue: 'US|SS', + LargestImagePixelValue: 'US|SS', + PixelData: 'OB|OW', + }, + InstanceCreationTime: '160738', + InstanceCreatorUID: '2.16.124.113543.6004.101.103', + SOPInstanceUID: '1.3.6.1.4.1.9590.100.1.2.304484424913537637032577967974004238574', + AcquisitionTime: '160738', + ContentTime: '160738', + AdmittingDiagnosesDescription: 'NA', + DerivationDescription: 'NA', + AdditionalPatientHistory: + 'Images from the 2002 AAPM task group report on display performan', + PatientComments: 'NA', + TimeOfSecondaryCapture: '160738', + DigitalImageFormatAcquired: 'Image exported from tShow application', + InstanceNumber: 1, + Laterality: 'NA', + ImagesInAcquisition: 1, + SmallestImagePixelValue: { InlineBinary: 'AAA=' }, + LargestImagePixelValue: { InlineBinary: '/w8=' }, + PixelData: { + BulkDataURI: + 'instances/1.3.6.1.4.1.9590.100.1.2.304484424913537637032577967974004238574/frames', + }, + AvailableTransferSyntaxUID: '1.2.840.10008.1.2.4.80', + '00090010': 'dedupped', + '00091010': [ + '0f4205417d9d2658a75f28b85651939a9832e5649ef0475ad19a3d53956ac9a0', + '602b9c21e612ba224493bcbf1a867bde745371e7b60dd6a149fe6e8d7bba5af9', + '93df3408efa1dc2e9631eb030a9ac802b0844b3c03606ecc597dcb12e0c6e812', + '5ae910ca86c68a269dd52aeae2d5faa167bc76b6474c463ebd8757c6a8f3758a', + ], + SeriesInstanceUID: '2.16.124.113543.6004.101.103.20021117.190619.1.001', + '00091011': '988f02f735e0fc7372ff4e7373877689529135e238712ecc664961d0d3f20c27', + '00091012': 'instance', + PatientID: 'TG18-2002', + PatientName: [{ Alphabetic: 'AAPM^Test^Patterns' }], + PatientBirthDate: '20020704', + PatientSex: 'O', + StudyDescription: 'Multi Purpose 1K', + AccessionNumber: '20022002', + StudyInstanceUID: '2.16.124.113543.6004.101.103.20021117.190619.1', + StudyDate: '20180724', + StudyTime: '190619', + StudyID: '1K-MULTI', + SeriesDescription: 'TG18-OIQ', + SeriesNumber: 1, + Modality: 'OT', + SeriesDate: '20180724', + SeriesTime: '160738', + ReferringPhysicianName: [{ Alphabetic: 'AAPM' }], + BodyPartExamined: 'NA', + ProtocolName: 'Display Quality Test Protocol', + ImageType: 'ORIGINAL', + InstanceCreationDate: '20180724', + SOPClassUID: '1.2.840.10008.5.1.4.1.1.7', + ConversionType: 'WSD', + ContentDate: '20180724', + AcquisitionDate: '20180724', + DateOfSecondaryCapture: '20180724', + SecondaryCaptureDeviceManufacturer: 'Duke University Health System', + SecondaryCaptureDeviceManufacturerModelName: 'MATLAB', + SamplesPerPixel: 1, + PhotometricInterpretation: 'MONOCHROME2', + Rows: 1280, + Columns: 1024, + BitsAllocated: 16, + BitsStored: 16, + HighBit: 15, + PixelRepresentation: 0, + PatientOrientation: null, + imageId: + 'wadors:https://d14fa38qiwhyfd.cloudfront.net/dicomweb/studies/2.16.124.113543.6004.101.103.20021117.190619.1/series/2.16.124.113543.6004.101.103.20021117.190619.1.001/instances/1.3.6.1.4.1.9590.100.1.2.304484424913537637032577967974004238574/frames/1', + wadoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + }, + ], + }, +}; +export const qidoInstanceMetadata = [ + { + '00080018': { + Value: ['1.3.6.1.4.1.9590.100.1.2.346385697213345623712464894863342033573'], + }, + '00200013': { + vr: 'IS', + Value: [1], + }, + '0020000E': { + Value: ['2.16.124.113543.6004.101.103.20021117.190619.1.001'], + }, + '0020000D': { + vr: 'UI', + Value: ['2.16.124.113543.6004.101.103.20021117.190619.1'], + }, + '00080023': { + vr: 'DA', + Value: ['20180724'], + }, + '00080033': { + vr: 'TM', + Value: ['110724'], + }, + '00080022': { + vr: 'DA', + Value: ['20180724'], + }, + '00080032': { + vr: 'TM', + Value: ['110724'], + }, + '00280002': { + vr: 'US', + Value: [1], + }, + '00280004': { + vr: 'CS', + Value: ['MONOCHROME2'], + }, + '00280010': { + vr: 'US', + Value: [1024], + }, + '00280011': { + vr: 'US', + Value: [1024], + }, + '00280100': { + vr: 'US', + Value: [16], + }, + '00280101': { + vr: 'US', + Value: [16], + }, + '00280102': { + vr: 'US', + Value: [15], + }, + '00280103': { + vr: 'US', + Value: [0], + }, + '00080013': { + vr: 'TM', + Value: ['110724'], + }, + '00090010': { + Value: ['dedupped'], + vr: 'CS', + }, + '00091011': { + Value: ['e3d8e23130c980066d92cd54f24ffe9999ec0bfee40b96a6e15cc364f58e0e37'], + }, + '00091012': { + Value: ['instance'], + }, + }, + { + '00080018': { + Value: ['1.3.6.1.4.1.9590.100.1.2.294498401812162035928148179312426786986'], + }, + '00200013': { + vr: 'IS', + Value: [1], + }, + '0020000E': { + Value: ['2.16.124.113543.6004.101.103.20021117.190619.1.001'], + }, + '0020000D': { + vr: 'UI', + Value: ['2.16.124.113543.6004.101.103.20021117.190619.1'], + }, + '00080023': { + vr: 'DA', + Value: ['20180724'], + }, + '00080033': { + vr: 'TM', + Value: ['140724'], + }, + '00080022': { + vr: 'DA', + Value: ['20180724'], + }, + '00080032': { + vr: 'TM', + Value: ['140724'], + }, + '00280002': { + vr: 'US', + Value: [1], + }, + '00280004': { + vr: 'CS', + Value: ['MONOCHROME2'], + }, + '00280010': { + vr: 'US', + Value: [1024], + }, + '00280011': { + vr: 'US', + Value: [1280], + }, + '00280100': { + vr: 'US', + Value: [16], + }, + '00280101': { + vr: 'US', + Value: [16], + }, + '00280102': { + vr: 'US', + Value: [15], + }, + '00280103': { + vr: 'US', + Value: [0], + }, + '00080013': { + vr: 'TM', + Value: ['140724'], + }, + '00090010': { + Value: ['dedupped'], + vr: 'CS', + }, + '00091011': { + Value: ['40b4277bb9588f7a4406f29b9f9046794b6730f35346cc9cdc1b69e98373f61c'], + }, + '00091012': { + Value: ['instance'], + }, + }, + { + '00080018': { + Value: ['1.3.6.1.4.1.9590.100.1.2.304484424913537637032577967974004238574'], + }, + '00200013': { + vr: 'IS', + Value: [1], + }, + '0020000E': { + Value: ['2.16.124.113543.6004.101.103.20021117.190619.1.001'], + }, + '0020000D': { + vr: 'UI', + Value: ['2.16.124.113543.6004.101.103.20021117.190619.1'], + }, + '00080023': { + vr: 'DA', + Value: ['20180724'], + }, + '00080033': { + vr: 'TM', + Value: ['160738'], + }, + '00080022': { + vr: 'DA', + Value: ['20180724'], + }, + '00080032': { + vr: 'TM', + Value: ['160738'], + }, + '00280002': { + vr: 'US', + Value: [1], + }, + '00280004': { + vr: 'CS', + Value: ['MONOCHROME2'], + }, + '00280010': { + vr: 'US', + Value: [1280], + }, + '00280011': { + vr: 'US', + Value: [1024], + }, + '00280100': { + vr: 'US', + Value: [16], + }, + '00280101': { + vr: 'US', + Value: [16], + }, + '00280102': { + vr: 'US', + Value: [15], + }, + '00280103': { + vr: 'US', + Value: [0], + }, + '00080013': { + vr: 'TM', + Value: ['160738'], + }, + '00090010': { + Value: ['dedupped'], + vr: 'CS', + }, + '00091011': { + Value: ['8ba1bb7cbda839482ad9ae517abcfc9cb5d11a8045d69a1069d784c90cf97434'], + }, + '00091012': { + Value: ['instance'], + }, + }, +]; +export const naturalizedQidoInstances = dicomWebToDicomStructure(qidoInstanceMetadata); +export const expectedInstanceMetadata = [ + { + _vrMap: { + SmallestImagePixelValue: 'US|SS', + LargestImagePixelValue: 'US|SS', + PixelData: 'OB|OW', + }, + InstanceCreationTime: '110724', + InstanceCreatorUID: '2.16.124.113543.6004.101.103', + AcquisitionTime: '110724', + ContentTime: '110724', + AdmittingDiagnosesDescription: 'NA', + DerivationDescription: 'NA', + AdditionalPatientHistory: 'Images from the 2002 AAPM task group report on display performan', + PatientComments: 'NA', + TimeOfSecondaryCapture: '110724', + DigitalImageFormatAcquired: 'Image exported from tShow application', + InstanceNumber: 0, + Laterality: 'NA', + ImagesInAcquisition: 1, + SmallestImagePixelValue: { InlineBinary: 'AAA=' }, + LargestImagePixelValue: { InlineBinary: '/w8=' }, + PixelData: { + BulkDataURI: + 'instances/1.3.6.1.4.1.9590.100.1.2.346385697213345623712464894863342033573/frames', + }, + AvailableTransferSyntaxUID: '1.2.840.10008.1.2.4.80', + '00090010': 'dedupped', + '00091010': [ + '0f4205417d9d2658a75f28b85651939a9832e5649ef0475ad19a3d53956ac9a0', + '602b9c21e612ba224493bcbf1a867bde745371e7b60dd6a149fe6e8d7bba5af9', + 'dc77149ddb654f34e3fe66cb443495acb2b60fbc18ded168e31e0fc64147eeb9', + '36bf58abfb35d6a35cdf7f077d92d80980703c13e590e2074819c95428eac7cb', + ], + '00091011': '28b44927e9e3ec0003e5183bcb817c6ada9a84085b0e8ced4550a4c6be6cbaed', + '00091012': 'instance', + PatientID: 'TG18-2002', + PatientName: [{ Alphabetic: 'AAPM^Test^Patterns' }], + PatientBirthDate: '20020704', + PatientSex: 'O', + StudyDescription: 'Multi Purpose 1K', + AccessionNumber: '20022002', + StudyDate: '20180724', + StudyTime: '190619', + StudyID: '1K-MULTI', + SeriesDescription: 'TG18-OIQ', + SeriesNumber: 1, + Modality: 'OT', + SeriesDate: '20180724', + SeriesTime: '110724', + ReferringPhysicianName: [{ Alphabetic: 'AAPM' }], + BodyPartExamined: 'NA', + ProtocolName: 'Display Quality Test Protocol', + ImageType: 'ORIGINAL', + InstanceCreationDate: '20180724', + ConversionType: 'WSD', + ContentDate: '20180724', + AcquisitionDate: '20180724', + DateOfSecondaryCapture: '20180724', + SecondaryCaptureDeviceManufacturer: 'Duke University Health System', + SecondaryCaptureDeviceManufacturerModelName: 'MATLAB', + SamplesPerPixel: 1, + PhotometricInterpretation: 'MONOCHROME2', + BitsStored: 16, + HighBit: 15, + PixelRepresentation: 0, + PatientOrientation: null, + }, + { + _vrMap: { + SmallestImagePixelValue: 'US|SS', + LargestImagePixelValue: 'US|SS', + PixelData: 'OB|OW', + }, + InstanceCreationTime: '110724', + InstanceCreatorUID: '2.16.124.113543.6004.101.103', + AcquisitionTime: '110724', + ContentTime: '110724', + AdmittingDiagnosesDescription: 'NA', + DerivationDescription: 'NA', + AdditionalPatientHistory: 'Images from the 2002 AAPM task group report on display performan', + PatientComments: 'NA', + TimeOfSecondaryCapture: '110724', + DigitalImageFormatAcquired: 'Image exported from tShow application', + InstanceNumber: 1, + Laterality: 'NA', + ImagesInAcquisition: 1, + SmallestImagePixelValue: { InlineBinary: 'AAA=' }, + LargestImagePixelValue: { InlineBinary: '/w8=' }, + PixelData: { + BulkDataURI: + 'instances/1.3.6.1.4.1.9590.100.1.2.346385697213345623712464894863342033573/frames', + }, + AvailableTransferSyntaxUID: '1.2.840.10008.1.2.4.80', + '00090010': 'dedupped', + '00091010': [ + '0f4205417d9d2658a75f28b85651939a9832e5649ef0475ad19a3d53956ac9a0', + '602b9c21e612ba224493bcbf1a867bde745371e7b60dd6a149fe6e8d7bba5af9', + 'dc77149ddb654f34e3fe66cb443495acb2b60fbc18ded168e31e0fc64147eeb9', + '36bf58abfb35d6a35cdf7f077d92d80980703c13e590e2074819c95428eac7cb', + ], + '00091011': '28b44927e9e3ec0003e5183bcb817c6ada9a84085b0e8ced4550a4c6be6cbaed', + '00091012': 'instance', + PatientID: 'TG18-2002', + PatientName: [{ Alphabetic: 'AAPM^Test^Patterns' }], + PatientBirthDate: '20020704', + PatientSex: 'O', + StudyDescription: 'Multi Purpose 1K', + AccessionNumber: '20022002', + StudyDate: '20180724', + StudyTime: '190619', + StudyID: '1K-MULTI', + SeriesDescription: 'TG18-OIQ', + SeriesNumber: 1, + Modality: 'OT', + SeriesDate: '20180724', + SeriesTime: '110724', + ReferringPhysicianName: [{ Alphabetic: 'AAPM' }], + BodyPartExamined: 'NA', + ProtocolName: 'Display Quality Test Protocol', + ImageType: 'ORIGINAL', + InstanceCreationDate: '20180724', + ConversionType: 'WSD', + ContentDate: '20180724', + AcquisitionDate: '20180724', + DateOfSecondaryCapture: '20180724', + SecondaryCaptureDeviceManufacturer: 'Duke University Health System', + SecondaryCaptureDeviceManufacturerModelName: 'MATLAB', + SamplesPerPixel: 1, + PhotometricInterpretation: 'MONOCHROME2', + BitsStored: 16, + HighBit: 15, + PixelRepresentation: 0, + PatientOrientation: null, + }, +]; + +describe('DicomWebDataSource Test Data', () => { + test('should be able to initialize client', () => { + expect(() => { + console.log(client); + }); + }); + + test('should have naturalized instances', () => { + expect(() => { + console.log(naturalizedInstances); + }); + }); +}); diff --git a/extensions/default/src/DataSources/utils/dicom.ts b/extensions/default/src/DataSources/utils/dicom.ts new file mode 100644 index 00000000000..f2df6c38160 --- /dev/null +++ b/extensions/default/src/DataSources/utils/dicom.ts @@ -0,0 +1,11 @@ +/** + * Basic imports from dcmjs. + */ + +import dcmjs from 'dcmjs'; + +const { DicomMetaDictionary, DicomDict } = dcmjs.data; + +const { naturalizeDataset, denaturalizeDataset } = DicomMetaDictionary; + +export {DicomMetaDictionary, DicomDict, naturalizeDataset, denaturalizeDataset}; diff --git a/extensions/default/src/DataSources/utils/dicomWebConfig.ts b/extensions/default/src/DataSources/utils/dicomWebConfig.ts new file mode 100644 index 00000000000..2f6478dd676 --- /dev/null +++ b/extensions/default/src/DataSources/utils/dicomWebConfig.ts @@ -0,0 +1,70 @@ + +export type DicomWebConfig = { + /** Data source name */ + name: string; + // wadoUriRoot - Legacy? (potentially unused/replaced) + /** Base URL to use for QIDO requests */ + qidoRoot?: string; + wadoRoot?: string; // - Base URL to use for WADO requests + wadoUri?: string; // - Base URL to use for WADO URI requests + qidoSupportsIncludeField?: boolean; // - Whether QIDO supports the "Include" option to request additional fields in response + imageRendering?: string; // - wadors | ? (unsure of where/how this is used) + thumbnailRendering?: string; + /** + wadors - render using the wadors fetch. The full image is retrieved and rendered in cornerstone to thumbnail size png and returned as binary data to the src attribute of the image tag. + for example, + thumbnailDirect - get the direct url endpoint for the thumbnail as the image src (eg not authentication required). + for example, + thumbnail - render using the thumbnail endpoint on wadors using bulkDataURI, passing authentication params to the url. + rendered - should use the rendered endpoint instead of the thumbnail endpoint + */ + /** Whether the server supports reject calls (i.e. DCM4CHEE) */ + supportsReject?: boolean; + /** indicates if the retrieves can fetch singlepart. Options are bulkdata, video, image, or true */ + singlepart?: boolean | string; + /** Transfer syntax to request from the server */ + requestTransferSyntaxUID?: string; + acceptHeader?: string[]; // - Accept header to use for requests + /** Whether to omit quotation marks for multipart requests */ + omitQuotationForMultipartRequest?: boolean; + /** Whether the server supports fuzzy matching */ + supportsFuzzyMatching?: boolean; + /** Whether the server supports wildcard matching */ + supportsWildcard?: boolean; + /** Whether the server supports the native DICOM model */ + supportsNativeDICOMModel?: boolean; + /** Whether to enable request tag */ + enableRequestTag?: boolean; + /** Whether to enable study lazy loading */ + enableStudyLazyLoad?: boolean; + /** Whether to enable bulkDataURI */ + bulkDataURI?: BulkDataURIConfig; + /** Function that is called after the configuration is initialized */ + onConfiguration: (config: DicomWebConfig, params) => DicomWebConfig; + /** Whether to use the static WADO client */ + staticWado?: boolean; + /** User authentication service */ + userAuthenticationService: Record; +}; + +export type BulkDataURIConfig = { + /** Enable bulkdata uri configuration */ + enabled?: boolean; + /** + * Remove the startsWith string. + * This is used to correct reverse proxied URLs by removing the startsWith path + */ + startsWith?: string; + /** + * Adds this prefix path. Only used if the startsWith is defined and has + * been removed. This allows replacing the base path. + */ + prefixWith?: string; + /** Transform the bulkdata path. Used to replace a portion of the path */ + transform?: (uri: string) => string; + /** + * Adds relative resolution to the path handling. + * series is the default, as the metadata retrieved is series level. + */ + relativeResolution?: 'studies' | 'series'; +}; \ No newline at end of file diff --git a/extensions/default/src/DicomWebDataSource/utils/findIndexOfString.ts b/extensions/default/src/DataSources/utils/findIndexOfString.ts similarity index 100% rename from extensions/default/src/DicomWebDataSource/utils/findIndexOfString.ts rename to extensions/default/src/DataSources/utils/findIndexOfString.ts diff --git a/extensions/default/src/DicomWebDataSource/utils/fixBulkDataURI.ts b/extensions/default/src/DataSources/utils/fixBulkDataURI.ts similarity index 100% rename from extensions/default/src/DicomWebDataSource/utils/fixBulkDataURI.ts rename to extensions/default/src/DataSources/utils/fixBulkDataURI.ts diff --git a/extensions/default/src/DicomWebDataSource/utils/fixMultiValueKeys.ts b/extensions/default/src/DataSources/utils/fixMultiValueKeys.ts similarity index 100% rename from extensions/default/src/DicomWebDataSource/utils/fixMultiValueKeys.ts rename to extensions/default/src/DataSources/utils/fixMultiValueKeys.ts diff --git a/extensions/default/src/DicomWebDataSource/utils/fixMultipart.ts b/extensions/default/src/DataSources/utils/fixMultipart.ts similarity index 100% rename from extensions/default/src/DicomWebDataSource/utils/fixMultipart.ts rename to extensions/default/src/DataSources/utils/fixMultipart.ts diff --git a/extensions/default/src/DataSources/utils/getImageId.test.ts b/extensions/default/src/DataSources/utils/getImageId.test.ts new file mode 100644 index 00000000000..c14c2ac39a9 --- /dev/null +++ b/extensions/default/src/DataSources/utils/getImageId.test.ts @@ -0,0 +1,26 @@ +import { + getImageId +} from './getImageId'; +import { + dicomWebConfig, + naturalizedInstances, +} from './data.test'; + +describe('getImageId', () => { + + + it('make sure we generate the correct image id', () => { + const instance = Array.from(naturalizedInstances).pop(); + const expected = 'wadors:https://d14fa38qiwhyfd.cloudfront.net/dicomweb/studies/2.16.124.113543.6004.101.103.20021117.190619.1/series/2.16.124.113543.6004.101.103.20021117.190619.1.001/instances/1.3.6.1.4.1.9590.100.1.2.304484424913537637032577967974004238574/frames/1'; + const result = getImageId(instance, null, dicomWebConfig); + expect(result).toStrictEqual(expected); + }); + + + it('make sure we generate the correct image id with frame specified', () => { + const instance = Array.from(naturalizedInstances).pop(); + const expected = 'wadors:https://d14fa38qiwhyfd.cloudfront.net/dicomweb/studies/2.16.124.113543.6004.101.103.20021117.190619.1/series/2.16.124.113543.6004.101.103.20021117.190619.1.001/instances/1.3.6.1.4.1.9590.100.1.2.304484424913537637032577967974004238574/frames/2'; + const result = getImageId(instance, 2, dicomWebConfig); + expect(result).toStrictEqual(expected); + }); +}); diff --git a/extensions/default/src/DicomWebDataSource/utils/getImageId.js b/extensions/default/src/DataSources/utils/getImageId.ts similarity index 80% rename from extensions/default/src/DicomWebDataSource/utils/getImageId.js rename to extensions/default/src/DataSources/utils/getImageId.ts index b3dcef097e4..2c0d4d33b20 100644 --- a/extensions/default/src/DicomWebDataSource/utils/getImageId.js +++ b/extensions/default/src/DataSources/utils/getImageId.ts @@ -1,4 +1,6 @@ import getWADORSImageId from './getWADORSImageId'; +import {DicomReferenceMetadata} from './Types'; +import { DicomWebConfig } from './dicomWebConfig'; function buildInstanceWadoUrl(config, instance) { const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance; @@ -24,8 +26,13 @@ function buildInstanceWadoUrl(config, instance) { * @param thumbnail * @returns {string} The imageId to be used by Cornerstone */ -export default function getImageId({ instance, frame, config, thumbnail = false }) { - if (!instance) { +export function getImageId( + instance: DicomReferenceMetadata, + frame: number | undefined, + config: DicomWebConfig, + thumbnail: boolean = false +): string | undefined { + if (!instance || !config) { return; } @@ -52,3 +59,5 @@ export default function getImageId({ instance, frame, config, thumbnail = false return getWADORSImageId(instance, config, frame); // WADO-RS Retrieve Frame } } + +export default { getImageId }; diff --git a/extensions/default/src/DicomWebDataSource/utils/getWADORSImageId.js b/extensions/default/src/DataSources/utils/getWADORSImageId.js similarity index 100% rename from extensions/default/src/DicomWebDataSource/utils/getWADORSImageId.js rename to extensions/default/src/DataSources/utils/getWADORSImageId.js diff --git a/extensions/default/src/DataSources/utils/headers.ts b/extensions/default/src/DataSources/utils/headers.ts new file mode 100644 index 00000000000..ee876450c28 --- /dev/null +++ b/extensions/default/src/DataSources/utils/headers.ts @@ -0,0 +1,59 @@ +import { HeadersInterface } from '@ohif/core/src/types/RequestHeaders'; +import { utils } from '@ohif/core'; + +/** + * Options from the configuration file that apply to those functions below that expect them. + * + * For example, see generateWadoHeader. + */ +export interface HeaderOptions { + acceptHeader?: string[], + requestTransferSyntaxUID?: string, + omitQuotationForMultipartRequest?: boolean +} + +/** + * Generates the basic authentication header needed when making requests to the Dicom endpoint. + * @param userAuthenticationService + */ +export function generateAuthorizationHeader(userAuthenticationService): HeadersInterface { + const xhrRequestHeaders: HeadersInterface = {}; + const authHeaders = userAuthenticationService.getAuthorizationHeader(); + if (authHeaders && authHeaders.Authorization) { + xhrRequestHeaders.Authorization = authHeaders.Authorization; + } + return xhrRequestHeaders; +} + +/** + * Generates a header for a WADO request. You can choose to skip the inclusion of Accept header options + * present in the dicomweb config section of your configuration file. You can do so by toggling + * the includeTransferSyntax parameter. + * + * @param userAuthenticationService + * @param options + * @param includeTransferSyntax + */ +export function generateWadoHeader( + userAuthenticationService, + options: HeaderOptions, + includeTransferSyntax: boolean = false +): HeadersInterface { + const authorizationHeader = generateAuthorizationHeader(userAuthenticationService); + if (includeTransferSyntax) { + //Generate accept header depending on config params + const formattedAcceptHeader = utils.generateAcceptHeader( + options.acceptHeader, + options.requestTransferSyntaxUID, + options.omitQuotationForMultipartRequest + ); + return { + ...authorizationHeader, + Accept: formattedAcceptHeader, + }; + } else { + return { + ...authorizationHeader + }; + } +} \ No newline at end of file diff --git a/extensions/default/src/DataSources/utils/index.ts b/extensions/default/src/DataSources/utils/index.ts new file mode 100644 index 00000000000..eb76273f8a1 --- /dev/null +++ b/extensions/default/src/DataSources/utils/index.ts @@ -0,0 +1,63 @@ +export type {HeadersInterface} from '@ohif/core/src/types/RequestHeaders'; +export { generateAuthorizationHeader, generateWadoHeader } from './headers'; +export type { HeaderOptions } from './headers'; + +export { fixMultiValueKeys } from './fixMultiValueKeys'; +export { fixBulkDataURI } from './fixBulkDataURI'; +export { + cleanDenaturalizedDataset, + transferDenaturalizedDataset +} from './cleanDenaturalizedDataset'; + +export type { DicomWebConfig, BulkDataURIConfig } from './dicomWebConfig'; +export type { RetrieveStudyMetadataInterface } from './Types'; + +/** + * Collect the main exports for WADO + */ +export { + retrieveStudyMetadata, + deleteStudyMetadataPromise, + retrieveInstanceMetadata, + retrieveMinimalSeriesMetadata, + retrieveFullSeriesMetadata, + retrieveSeriesMetadataAsync +} from './wado'; + +/** + * Collect the main exports for QIDO + */ +export { + mapParams, + search as qidoSearch, + seriesInStudy, + processResults, + processSeriesResults, + listSeries, + listSeriesInstances, +} from './qido'; + +/** + * Collect metadata processing imports + */ +export { + generateInstanceMetaData, + generateStudyMetaData, + dicomWebToDicomStructure, + dicomWebToRawDicomInstances, +} from './metadata/extractMetaData'; + +/** + * Collect other used imports. + */ +export {getImageId} from './getImageId'; +export {addRetrieveBulkData} from './wado/retrieveBulkData'; + +import StaticWadoClient from './StaticWadoClient'; +import getDirectURL from '../../utils/getDirectURL'; +export {getDirectURL, StaticWadoClient}; + +/** + * Collect dicomjs based imports. + */ +export {DicomMetaDictionary, DicomDict, naturalizeDataset, denaturalizeDataset} from './dicom' diff --git a/extensions/default/src/DataSources/utils/metadata/extractMetaData.test.ts b/extensions/default/src/DataSources/utils/metadata/extractMetaData.test.ts new file mode 100644 index 00000000000..5cdc0dbad39 --- /dev/null +++ b/extensions/default/src/DataSources/utils/metadata/extractMetaData.test.ts @@ -0,0 +1,41 @@ +import { + dicomWebToDicomStructure, + generateInstanceMetaData, + generateStudyMetaData, +} from './extractMetaData'; +import { + dicomInstances, + naturalizedInstances, + dicomWebConfig, + expectedStudyMetadata, + naturalizedQidoInstances, + expectedInstanceMetadata, +} from '../data.test'; +import { DicomSeriesStructureData, RawDicomInstances } from '../Types'; + +describe('extractMetaData', () => { + + + it('convert DICOMWeb JSON objects to parsed objects', () => { + const result = dicomWebToDicomStructure(dicomInstances); + expect(JSON.stringify(result)).toStrictEqual(JSON.stringify(naturalizedInstances)); + }); + + + it('generate study metadata structure', () => { + const dicomSeriesInstances: DicomSeriesStructureData = [naturalizedInstances]; + const result = generateStudyMetaData(dicomSeriesInstances, dicomWebConfig); + expect(JSON.stringify(result)).toStrictEqual(JSON.stringify(expectedStudyMetadata)); + }); + + + it('generate instance metadata structure', () => { + const qidoInstances = Array.from(naturalizedQidoInstances); + const wadoInstances = Array.from(dicomInstances); + const dicomQidoInstances = [[qidoInstances.shift(), qidoInstances.pop()]]; + const dicomWadoInstances = [[wadoInstances.shift(), wadoInstances.pop()]]; + const result = generateInstanceMetaData(dicomQidoInstances, dicomWadoInstances); + + expect(JSON.stringify(result)).toStrictEqual(JSON.stringify([expectedInstanceMetadata])); + }); +}); diff --git a/extensions/default/src/DataSources/utils/metadata/extractMetaData.ts b/extensions/default/src/DataSources/utils/metadata/extractMetaData.ts new file mode 100644 index 00000000000..2521b9b3329 --- /dev/null +++ b/extensions/default/src/DataSources/utils/metadata/extractMetaData.ts @@ -0,0 +1,240 @@ +/** + * Tools for extracting key metadata items from QIDO and WADO queries such that it is digestible + * by the viewer. + */ +import { getImageId } from '../getImageId'; +import { DicomWebConfig } from '../dicomWebConfig'; +import { + DicomReferenceMetadata, + DicomSeriesHeaderMetaData, + DicomSeriesStructureData, + DicomStructureData, + DicomStudyMetaData, + RawDicomInstances, +} from '../Types'; +import {naturalizeDataset} from '../dicom' + +/** + * Takes a list of settled promises containing fulfilled promises and returns a list of lists of + * fulfilled promises. + * + * The goal here is to take the data returned by `dicomweb-client`, which lacks type information and + * begin adding type annotations. + * + * We also want to coerce the input into an array of instances. + * + * @param instances list of settled promises containing fulfilled promises + */ +export function dicomWebToRawDicomInstances(instances: Promise|any[]): RawDicomInstances { + const rawInstances = instances.value ? instances.value : instances + return rawInstances.map((promise) => promise.value ? promise.value : promise); +} + + +/** + * Converts a list of a list of fulfilled promises into a list of `DicomStructure`. + * This function calls the `dcmjs` `naturalizeDataset` function to cast the input into a DICOM + * structure. I then take this structure and push it into a list that assumes the input is a + * `DicomStructure`. From this point on, the metadata of interest is annotated for linter checks. + * + * @param data list of a list of fulfilled promises + */ +export function dicomWebToDicomStructure(data: RawDicomInstances): DicomStructureData { + let naturalizedInstancesMetadata: DicomStructureData = []; + data.forEach((seriesInstances) => { + // This should be a single layer of promise => { status: "fulfilled", value: [{tags...}] } + if (seriesInstances && typeof seriesInstances === 'object' && "value" in seriesInstances) { + return seriesInstances.value.map((instance) => { + naturalizedInstancesMetadata.push(naturalizeDataset(instance)); + }); + // If we are getting a list of lists of raw dicom instances from a wado client. + } else if (Array.isArray(seriesInstances)) { + return seriesInstances.map((instance) => { + naturalizedInstancesMetadata.push(naturalizeDataset(instance)); + }); + // If we are getting a list of raw instances from a wado client. + } else { + return naturalizedInstancesMetadata.push(naturalizeDataset(seriesInstances)); + } + }); + + return naturalizedInstancesMetadata; +} + +/** + * Goes through each input instances and generates a series summary header and sorts the instances. + * The idea is to organize the data into two lists. One list for the series metadata header and + * one list for the instances in `DicomStructure` layout. + * + * Something else we do is generate an image ID to be associated with the input the instance. + * We need the system configuration for this process. + * We also use the configuration to associate the WADO root and uri urls to the instances for later + * instance retrieval. + * + * @param data list of list of `DicomStructure` instances + * @param dicomWebConfig reference to system configuration to adjust the instance metadata. + */ +export function generateStudyMetaData( + data: DicomSeriesStructureData, + dicomWebConfig: DicomWebConfig +): DicomStudyMetaData +{ + const seriesSummaryMetadata = new Map(); + const instancesPerSeries = new Map(); + + data.forEach((series: DicomStructureData) => { + series.forEach((instance) => { + const seriesInstanceUID = instance.SeriesInstanceUID; + if (!seriesSummaryMetadata[seriesInstanceUID]) { + seriesSummaryMetadata[seriesInstanceUID] = { + StudyInstanceUID: instance.StudyInstanceUID, + StudyDescription: instance.StudyDescription, + SeriesInstanceUID: seriesInstanceUID, + SeriesDescription: instance.SeriesDescription, + SeriesNumber: instance.SeriesNumber, + SeriesTime: instance.SeriesTime, + SOPClassUID: instance.SOPClassUID, + ProtocolName: instance.ProtocolName, + Modality: instance.Modality, + }; + } + + if (!instancesPerSeries[seriesInstanceUID]) { + instancesPerSeries[seriesInstanceUID] = []; + } + + instance.imageId = getImageId( + instance, + undefined, + dicomWebConfig, + ); + instance.wadoRoot = dicomWebConfig.wadoRoot; + instance.wadoUri = dicomWebConfig.wadoUri; + + instancesPerSeries[instance.SeriesInstanceUID].push(instance); + }); + + }); + + return {seriesSummaryMetadata, instancesPerSeries} +} + +/** + * Patch the instance IPP assuming a uniform change between slices such that they are in roughly + * the correct location in space for 3D reconstruction. + * + * The IPP delta between slices in a series is typically in the z direction and uniform. + * To generate the new slice IPP, I use the first and last slices as guides to obtain the + * magnitude and direction of the change. All three axes are reconstructed. + * + * The IPP describes the general position of the slices in space relative to an orientation vector + * and frame of refence. + * + * Formula is IPPc = (lastc - firstc) / total_slices. + * + * @param firstSlice + * @param lastSlice + * @param indx + * @param totalSliceCount + */ +export function generateInstanceReferenceMetadata( + firstSlice: DicomReferenceMetadata, + lastSlice: DicomReferenceMetadata, + indx: number, + totalSliceCount: number, +): DicomReferenceMetadata +{ + const reference: DicomReferenceMetadata = JSON.parse(JSON.stringify(firstSlice)); + + // Compute slice IPP. Necessary for 3D reconstruction. We might need to account for gantry tilt + // Someone feel free to add corrections as needed + const firstIPP = firstSlice.ImagePositionPatient; + const lastIPP = lastSlice.ImagePositionPatient; + if (lastIPP) { + // Make sure all three components in the IPP are reconstructed accurately. + // Reconstruction of the z axis only is not enough (works for most cases though). + for (let i = 0; i < lastIPP.length; i++) { + reference.ImagePositionPatient[i] = generateIPPComponent(firstIPP[i], lastIPP[i], indx, totalSliceCount) + } + } + + return reference; +} + +/** + * Approximates the slice position in space along the given component based on initial and final + * slice values. + * + * Formula is IPPc = (lastc - firstc) / total_slices. + * + * @param {number} firstIPPComponent + * @param {number} lastIPPComponent + * @param {number} indx + * @param {number} totalSliceCount + */ +function generateIPPComponent( + firstIPPComponent: number, + lastIPPComponent: number, + indx: number, + totalSliceCount: number +) { + const deltaIPP = (lastIPPComponent - firstIPPComponent) / totalSliceCount; + return firstIPPComponent + indx * deltaIPP +} + +/** + * Using one of the WADO instances, generate the missing instance metadata structures needed by the + * Viewer to determine if 3D reconstruction is possible. + * + * This function uses the QIDO metadata to determine how many series to process and how many slices + * for each series to prepare. The QIDO metadata is first casted to a `DicomStructureData` for easy + * handling. + * + * The result is series => [first, last] >> [first, ..., last]. + * + * @param instanceQIDOMeta + * @param instanceWADOMeta + */ +export function generateInstanceMetaData ( + instanceQIDOMeta: any[], + instanceWADOMeta: RawDicomInstances +): DicomSeriesStructureData +{ + const newNaturalizedInstancesMetadata: DicomSeriesStructureData = []; + const naturalizedInstancesMetadata= instanceWADOMeta.map( + instances => dicomWebToDicomStructure(instances) + ); + + for(let i = 0; i < instanceQIDOMeta.length; i++) { + const referenceMetaData = naturalizedInstancesMetadata[i]; + const [firstSlice, lastSlice] = referenceMetaData; + const seriesInstances = instanceQIDOMeta[i]; + const newInstances: DicomStructureData = []; + const instances = seriesInstances.value ? seriesInstances.value : seriesInstances; + const totalSliceCount = instances.length; + + for (let i = 0; i < totalSliceCount; i++) { + const instance = instances[i]; + let newInstance = generateInstanceReferenceMetadata( + firstSlice, + lastSlice, + i, + totalSliceCount + ); + + newInstance.BitsAllocated = instance.bitsAllocated; + newInstance.Columns = instance.columns; + newInstance.Rows = instance.rows; + newInstance.InstanceNumber = i; + newInstance.SeriesInstanceUID = instance.seriesInstanceUID; + newInstance.SOPClassUID = instance.sopClassUID; + newInstance.SOPInstanceUID = instance.sopInstanceUID; + newInstance.StudyInstanceUID = instance.studyInstanceUID; + + newInstances.push(newInstance); + } + newNaturalizedInstancesMetadata.push(newInstances); + } + + return newNaturalizedInstancesMetadata; +} \ No newline at end of file diff --git a/extensions/default/src/DicomWebDataSource/qido.js b/extensions/default/src/DataSources/utils/qido/index.ts similarity index 60% rename from extensions/default/src/DicomWebDataSource/qido.js rename to extensions/default/src/DataSources/utils/qido/index.ts index ee13fa8c4b0..90daaf46867 100644 --- a/extensions/default/src/DicomWebDataSource/qido.js +++ b/extensions/default/src/DataSources/utils/qido/index.ts @@ -23,7 +23,7 @@ * | offset | {number} | */ import { DICOMWeb, utils } from '@ohif/core'; -import { sortStudySeries } from '@ohif/core/src/utils/sortStudy'; +import { sortStudySeries, sortStudyInstances } from '@ohif/core/src/utils/sortStudy'; const { getString, getName, getModalities } = DICOMWeb; @@ -57,7 +57,6 @@ function processResults(qidoStudies) { modalities: getString(getModalities(qidoStudy['00080060'], qidoStudy['00080061'])) || '', }) ); - return studies; } @@ -71,14 +70,14 @@ function processResults(qidoStudies) { * @param {string[]} qidoSeries[0].qidoSeries[dicomTag].Value - Optional string array representation of the DICOM Tag's value * @returns {Array} An array of Study MetaData objects */ -export function processSeriesResults(qidoSeries) { +export function processSeriesResults(qidoSeries, studyInstanceUid) { const series = []; if (qidoSeries && qidoSeries.length) { qidoSeries.forEach(qidoSeries => series.push({ - studyInstanceUid: getString(qidoSeries['0020000D']), - seriesInstanceUid: getString(qidoSeries['0020000E']), + studyInstanceUID: studyInstanceUid, + seriesInstanceUID: getString(qidoSeries['0020000E']), modality: getString(qidoSeries['00080060']), seriesNumber: getString(qidoSeries['00200011']), seriesDate: utils.formatDate(getString(qidoSeries['00080021'])), @@ -93,6 +92,40 @@ export function processSeriesResults(qidoSeries) { return series; } +/** + * Parses resulting data from a QIDO call into a set of Study MetaData + * + * @param {Array} qidoSeries - An array of study objects. Each object contains a keys for DICOM tags. + * @param {object} qidoSeries[0].qidoSeries - An object where each key is the DICOM Tag group+element + * @param {object} qidoSeries[0].qidoSeries[dicomTag] - Optional object that represents DICOM Tag + * @param {string} qidoSeries[0].qidoSeries[dicomTag].vr - Value Representation + * @param {string[]} qidoSeries[0].qidoSeries[dicomTag].Value - Optional string array representation of the DICOM Tag's value + * @returns {Array} An array of Study MetaData objects + */ +export function processInstancesResults(qidoInstances, studyInstanceUid, seriesInstanceUid) { + const instances = []; + + if (qidoInstances && qidoInstances.length) { + qidoInstances.forEach(qidoInstance => + instances.push({ + studyInstanceUID: studyInstanceUid, + seriesInstanceUID: seriesInstanceUid, + sopClassUID: getString(qidoInstance['00080016']), + sopInstanceUID: getString(qidoInstance['00080018']), + retrieveURL: getString(qidoInstance['00081190']), + instanceNumber: Number(getString(qidoInstance['00200013'])), + rows: Number(getString(qidoInstance['00280010'])), + columns: Number(getString(qidoInstance['00280011'])), + bitsAllocated: Number(getString(qidoInstance['00280100'])), + }) + ); + } + + sortStudyInstances(instances); + + return instances; +} + /** * * @param {object} dicomWebClient - Client similar to what's provided by `dicomweb-client` library @@ -111,6 +144,25 @@ async function search(dicomWebClient, studyInstanceUid, seriesInstanceUid, query return searchResult; } +/** + * + * @param {string} studyInstanceUID - ID of study to return a list of series for + * @returns {Promise} - Resolves SeriesMetadata[] in study + */ +async function studyInfo(dicomWebClient, studyInstanceUID) { + // Series Description + // Already included? + const commaSeparatedFields = ['0008103E', '00080021', '0020000D'].join(','); + const queryParams = { + StudyInstanceUID: studyInstanceUID, + includefield: commaSeparatedFields, + }; + + return await dicomWebClient.searchForStudies({ + queryParams: queryParams, + }); +} + /** * * @param {string} studyInstanceUID - ID of study to return a list of series for @@ -119,7 +171,7 @@ async function search(dicomWebClient, studyInstanceUid, seriesInstanceUid, query export function seriesInStudy(dicomWebClient, studyInstanceUID) { // Series Description // Already included? - const commaSeparatedFields = ['0008103E', '00080021'].join(','); + const commaSeparatedFields = ['0008103E', '00080021', '0020000D'].join(','); const queryParams = { includefield: commaSeparatedFields, }; @@ -127,6 +179,22 @@ export function seriesInStudy(dicomWebClient, studyInstanceUID) { return dicomWebClient.searchForSeries({ studyInstanceUID, queryParams }); } +/** + * + * @param {string} studyInstanceUID - ID of study to return a list of series for + * @returns {Promise} - Resolves SeriesMetadata[] in study + */ +export function instancesInSeries(dicomWebClient, studyInstanceUID, seriesInstanceUID) { + // Series Description + // Already included? + const commaSeparatedFields = ['0008103E', '00080021', '0020000E', '0020000D'].join(','); + const queryParams = { + includefield: commaSeparatedFields, + }; + + return dicomWebClient.searchForInstances({ studyInstanceUID, queryParams, seriesInstanceUID }); +} + export default function searchStudies(server, filter) { const queryParams = getQIDOQueryParams(filter, server.qidoSupportsIncludeField); const options = { @@ -212,4 +280,99 @@ function mapParams(params, options = {}) { return final; } -export { mapParams, search, processResults }; +async function StudyInfo( + dicomWebClient, + StudyInstanceUID, +){ + const results = await studyInfo(dicomWebClient, StudyInstanceUID); + + return processResults(results); +} + +async function listStudyInfo ( + dicomWebClient, + StudyInstanceUID +){ + let promise; + + // Create a promise to handle the data retrieval + promise = new Promise((resolve, reject) => { + StudyInfo( + dicomWebClient, + StudyInstanceUID + ).then(function(data) { + resolve(data); + }, reject); + }); + + return promise; +} + +async function ListSeries( + dicomWebClient, + StudyInstanceUID, +){ + const results = await seriesInStudy(dicomWebClient, StudyInstanceUID); + + return processSeriesResults(results, StudyInstanceUID) +} + +function listSeries ( + dicomWebClient, + StudyInstanceUID, + filters, +){ + let promise; + + if (filters && filters.seriesInstanceUID && Array.isArray(filters.seriesInstanceUID)) { + promise = ListSeries( + dicomWebClient, + StudyInstanceUID, + ); + } else { + // Create a promise to handle the data retrieval + promise = new Promise((resolve, reject) => { + ListSeries( + dicomWebClient, + StudyInstanceUID, + ).then(function(data) { + resolve(data); + }, reject); + }); + } + + return promise; +} + +async function ListSeriesInstances( + dicomWebClient, + StudyInstanceUID, + SeriesInstanceUID +){ + const results = await instancesInSeries(dicomWebClient, StudyInstanceUID, SeriesInstanceUID); + + return processInstancesResults(results, StudyInstanceUID, SeriesInstanceUID) +} + +function listSeriesInstances ( + dicomWebClient, + StudyInstanceUID, + SeriesInstanceUID, +){ + let promise; + + // Create a promise to handle the data retrieval + promise = new Promise((resolve, reject) => { + ListSeriesInstances( + dicomWebClient, + StudyInstanceUID, + SeriesInstanceUID, + ).then(function(data) { + resolve(data); + }, reject); + }); + + return promise; +} + +export { mapParams, search, processResults, listStudyInfo, listSeries, listSeriesInstances }; diff --git a/extensions/default/src/DataSources/utils/wado/index.ts b/extensions/default/src/DataSources/utils/wado/index.ts new file mode 100644 index 00000000000..3e8481e3f6c --- /dev/null +++ b/extensions/default/src/DataSources/utils/wado/index.ts @@ -0,0 +1,410 @@ +import { listStudyInfo, listSeries, listSeriesInstances } from '../qido'; +import { retrieveInstanceMetadata } from './retrieveInstanceMetadata'; +import { + dicomWebToDicomStructure, + dicomWebToRawDicomInstances, + generateInstanceMetaData, + generateStudyMetaData, +} from '../metadata/extractMetaData'; +import { DicomWebConfig } from '../dicomWebConfig'; +import {naturalizeDataset } from '../dicom'; +import { DicomMetadataStore, classes, UserAuthenticationService } from '@ohif/core'; +import { RetrieveStudyMetadataInterface } from '../Types'; +import { retrieveStudyMetadata, deleteStudyMetadataPromise } from './retrieveStudyMetadata'; +import { addRetrieveBulkData } from './retrieveBulkData'; +import {DICOMwebClient} from 'dicomweb-client/types/api'; +import { generateWadoHeader } from '../headers'; + +export type MetadataProvider = typeof classes.MetadataProvider; + +/** + * Minimum state to pass from an instance of a Dicom Data Source API instance so that we can execute + * metadata retrieval. + */ +export interface APIDependencies { + qidoDicomWebClient: DICOMwebClient; + wadoDicomWebClient: DICOMwebClient; + metadataProvider: MetadataProvider; + dicomWebConfig: DicomWebConfig; + userAuthenticationService?: UserAuthenticationService; + getImageIdsForInstance?: (arg0: {}) => string; +} + +/** + * Experimental threshold used to determine if to retrieve the full metadata bundle for the study + * or retrieve the bare minimum required for the viewer to function. This is part of an optimization + * effort. + */ +const fullMetadataThreshold = 10; + +/** + * Attempts to retrieve the minimum amount of metadata necessary to allow the viewer to operate. + * Because small bundles of metadata may be retrieved faster, we check if the study has enough slices + * before requesting individual chunks of metadata. If the study has too few slices, we default to + * the old behavior of retrieving the full metadata. + * + * @param {string} StudyInstanceUID + * @param {Object} filters + * @param {???} sortCriteria + * @param {Function} sortFunction + * @param {boolean} madeInClient + * @param {APIDependencies} api + */ +export async function retrieveMinimalSeriesMetadata ( + StudyInstanceUID, + filters, + sortCriteria, + sortFunction, + madeInClient, + api: APIDependencies +) { + const { + qidoDicomWebClient, + wadoDicomWebClient, + metadataProvider, + dicomWebConfig, + userAuthenticationService, + getImageIdsForInstance + } = api + const enableStudyLazyLoad = false; + + const studyInfo = (await listStudyInfo(qidoDicomWebClient, StudyInstanceUID)).pop(); + + if(studyInfo.instances <= fullMetadataThreshold) { + return retrieveFullSeriesMetadata( + StudyInstanceUID, + filters, + sortCriteria, + sortFunction, + madeInClient, + api + ) + } + + // Discover list of series in study + const seriesList = await listSeries( + qidoDicomWebClient, + StudyInstanceUID, + filters + ) + + // Discover list of instances in each series. + // We should have an array of arrays by now containing the raw QIDO metadata. + const instanceMetaList = await Promise.allSettled( + seriesList.map((series) => { + return listSeriesInstances( + qidoDicomWebClient, + StudyInstanceUID, + series.seriesInstanceUID + ) + }) + ); + + // For each series, retrieve the first and last instance metadata from the WADO interface. + // Unfortunately, we have to do this because the QIDO results lack the IPP and other information + // needed by the viewer to generate a hanging protocol. + let instances = await Promise.allSettled( + instanceMetaList.map( (instances) => { + const items = instances.value; + return Promise.allSettled( + [ + retrieveInstanceMetadata( + wadoDicomWebClient, + StudyInstanceUID, + enableStudyLazyLoad, + items[0], + sortCriteria, + sortFunction, + dicomWebConfig + ), + retrieveInstanceMetadata( + wadoDicomWebClient, + StudyInstanceUID, + enableStudyLazyLoad, + items[items.length - 1], + sortCriteria, + sortFunction, + dicomWebConfig + ), + ] + ); + }) + ); + + // Below, we want to grab the raw instance metadata list and generate a study structure such that + // we have a study global metadata header and a list of series. The list of series contains a list + // of instances per series. All of these instances have to be Proxy objects because of how + // dcmjs naturalizes the dataset. Also. note that since we only retrieved 2 slices worth of + // metadata per series, we have to "reconstruct" the other slices so the hanging protocol's requirements + // are satisfied! We do this by using the first slice as reference and then patch the IPP information. + const rawInstances = dicomWebToRawDicomInstances(instances); + const naturalizedInstancesMetadata= generateInstanceMetaData(instanceMetaList, rawInstances); + const { seriesSummaryMetadata, instancesPerSeries } = generateStudyMetaData( + naturalizedInstancesMetadata, + dicomWebConfig + ); + + // Now, register the study/images with tha metadata provider. + instancesPerSeries.forEach(instances => { + instances.forEach(instance => { + metadataProvider.addImageIdToUIDs(instance.imageId, { + StudyInstanceUID, + SeriesInstanceUID: instance.SeriesInstanceUID, + SOPInstanceUID: instance.SOPInstanceUID, + }); + }); + }); + + // Finally, store the series and instance data into the DicomMetadataStore. + const seriesMetadata = Object.values(seriesSummaryMetadata); + DicomMetadataStore.addSeriesMetadata(seriesMetadata, madeInClient); + + Object.keys(instancesPerSeries).forEach(seriesInstanceUID => + DicomMetadataStore.addInstances(instancesPerSeries[seriesInstanceUID], madeInClient) + ); + + // At this point the hanging protocol is notified of the data and things should begin drawing + // on screen. + // This function replaces `_retrieveSeriesMetadataSync` as a more optimal method of retrieval. + // Further optimization can be achieved if the DicomWeb standard supported an API like GraphQL. + // These changes improved load times for a study from seconds to a second overall and transfers + // of metadata decreased from 29 MB to 1.2MB overall. Image fetching is what slows down the system + // now. + return seriesSummaryMetadata; +} + +/** + * Downloads the full set of metadata as one chunk for a given study. This can be potentially expensive + * even with the aid of DEFLATE. I have seen as much as 29 MB transfers for a study with several CTs. + * + * @param {string} StudyInstanceUID + * @param {Object} filters + * @param {???} sortCriteria + * @param {Function} sortFunction + * @param {boolean} madeInClient + * @param {APIDependencies} api + */ +export async function retrieveFullSeriesMetadata ( + StudyInstanceUID, + filters, + sortCriteria, + sortFunction, + madeInClient, + api: APIDependencies +){ + const { + qidoDicomWebClient, + wadoDicomWebClient, + metadataProvider, + dicomWebConfig, + userAuthenticationService, + getImageIdsForInstance + } = api + const enableStudyLazyLoad = false; + // Skip inclusion of Accept Header options other than the request type of `application/dicom+json` + // See issue #5288 + wadoDicomWebClient.headers = generateWadoHeader( + userAuthenticationService, + dicomWebConfig, + false + ); + // data is all SOPInstanceUIDs + const data = await retrieveStudyMetadata( + wadoDicomWebClient, + StudyInstanceUID, + enableStudyLazyLoad, + filters, + sortCriteria, + sortFunction, + dicomWebConfig + ); + + // first naturalize the data + const naturalizedInstancesMetadata = data.map(naturalizeDataset); + + const seriesSummaryMetadata = {}; + const instancesPerSeries = {}; + + naturalizedInstancesMetadata.forEach(instance => { + if (!seriesSummaryMetadata[instance.SeriesInstanceUID]) { + seriesSummaryMetadata[instance.SeriesInstanceUID] = { + StudyInstanceUID: instance.StudyInstanceUID, + StudyDescription: instance.StudyDescription, + SeriesInstanceUID: instance.SeriesInstanceUID, + SeriesDescription: instance.SeriesDescription, + SeriesNumber: instance.SeriesNumber, + SeriesTime: instance.SeriesTime, + SOPClassUID: instance.SOPClassUID, + ProtocolName: instance.ProtocolName, + Modality: instance.Modality, + }; + } + + if (!instancesPerSeries[instance.SeriesInstanceUID]) { + instancesPerSeries[instance.SeriesInstanceUID] = []; + } + + const imageId = getImageIdsForInstance({ + instance, + }); + + instance.imageId = imageId; + instance.wadoRoot = dicomWebConfig.wadoRoot; + instance.wadoUri = dicomWebConfig.wadoUri; + + metadataProvider.addImageIdToUIDs(imageId, { + StudyInstanceUID, + SeriesInstanceUID: instance.SeriesInstanceUID, + SOPInstanceUID: instance.SOPInstanceUID, + }); + + instancesPerSeries[instance.SeriesInstanceUID].push(instance); + }); + + // grab all the series metadata + const seriesMetadata = Object.values(seriesSummaryMetadata); + DicomMetadataStore.addSeriesMetadata(seriesMetadata, madeInClient); + + Object.keys(instancesPerSeries).forEach(seriesInstanceUID => + DicomMetadataStore.addInstances(instancesPerSeries[seriesInstanceUID], madeInClient) + ); + + return seriesSummaryMetadata; +} + +/** + * Like retrieveFullSeriesMetadata, this function retrieves the metadata + * for the study. However, it does this asynchronously such that its effect is more like + * retrieveMinimalSeriesMetadata. The difference is that retrieveMinimalSeriesMetadata is synchronous + * (blocks), whereas this function does not block and thus gets you to the viewer UI faster. + * The main consideration is that the async calls started here relies on pre-flighting the request so + * beware of CORS conflicts if your environment is not configured correctly. + * + * @param {string} StudyInstanceUID + * @param {Object} filters + * @param {???} sortCriteria + * @param {Function} sortFunction + * @param {boolean} madeInClient + * @param {APIDependencies} api + */ +export async function retrieveSeriesMetadataAsync ( + StudyInstanceUID, + filters, + sortCriteria, + sortFunction, + api: APIDependencies, + madeInClient = false, + returnPromises = false +) { + const { + qidoDicomWebClient, + wadoDicomWebClient, + metadataProvider, + dicomWebConfig, + userAuthenticationService, + getImageIdsForInstance + } = api + const enableStudyLazyLoad = true; + // Skip inclusion of Accept Header options other than the request type of `application/dicom+json` + // See issue #5288 + wadoDicomWebClient.headers = generateWadoHeader( + userAuthenticationService, + dicomWebConfig, + false + ); + // Get Series + const results: RetrieveStudyMetadataInterface = + await retrieveStudyMetadata( + wadoDicomWebClient, + StudyInstanceUID, + enableStudyLazyLoad, + filters, + sortCriteria, + sortFunction, + dicomWebConfig + ); + const { preLoadData: seriesSummaryMetadata, promises: seriesPromises } = results; + + // Async load series, store as retrieved + function storeInstances(instances) { + const naturalizedInstances = dicomWebToDicomStructure(instances) + .map(instance => + addRetrieveBulkData( + instance, + qidoDicomWebClient, + dicomWebConfig + )); + + // Adding instanceMetadata to OHIF MetadataProvider + naturalizedInstances.forEach(instance => { + instance.wadoRoot = dicomWebConfig.wadoRoot; + instance.wadoUri = dicomWebConfig.wadoUri; + + const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance; + const numberOfFrames = instance.NumberOfFrames || 1; + // Process all frames consistently, whether single or multiframe + for (let i = 0; i < numberOfFrames; i++) { + const frameNumber = i + 1; + const frameImageId = getImageIdsForInstance({ + instance, + frameNumber, + }); + // Add imageId specific mapping to this data as the URL isn't necessarily WADO-URI. + metadataProvider.addImageIdToUIDs(frameImageId, { + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID, + frameNumber: numberOfFrames > 1 ? frameNumber : undefined, + }); + } + + // Adding imageId to each instance + // Todo: This is not the best way I can think of to let external + // metadata handlers know about the imageId that is stored in the store + const imageId = getImageIdsForInstance({ + instance, + }); + instance.imageId = imageId; + }); + + DicomMetadataStore.addInstances(naturalizedInstances, madeInClient); + } + + function setSuccessFlag() { + const study = DicomMetadataStore.getStudy(StudyInstanceUID); + if (!study) { + return; + } + study.isLoaded = true; + } + + // Google Cloud Healthcare doesn't return StudyInstanceUID, so we need to add + // it manually here + seriesSummaryMetadata.forEach(aSeries => { + aSeries.StudyInstanceUID = StudyInstanceUID; + }); + + DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata, madeInClient); + + const seriesDeliveredPromises = seriesPromises.map(promise => { + if (!returnPromises) { + promise?.start(); + } + + return promise.then(instances => { + storeInstances(instances); + }); + }); + + if (returnPromises) { + Promise.all(seriesDeliveredPromises).then(() => setSuccessFlag()); + return seriesPromises; + } else { + await Promise.all(seriesDeliveredPromises); + setSuccessFlag(); + } + + return seriesSummaryMetadata; +} + +export { retrieveStudyMetadata, deleteStudyMetadataPromise, retrieveInstanceMetadata } diff --git a/extensions/default/src/DataSources/utils/wado/retrieveBulkData.test.ts b/extensions/default/src/DataSources/utils/wado/retrieveBulkData.test.ts new file mode 100644 index 00000000000..b3941efc893 --- /dev/null +++ b/extensions/default/src/DataSources/utils/wado/retrieveBulkData.test.ts @@ -0,0 +1,38 @@ +import {addRetrieveBulkData, addRetrieveBulkDataNaturalized} from './retrieveBulkData'; +import { + naturalizedInstances, + client, + dicomWebConfig, + expectedNaturalizedInstances, + bulkDataURIExample, +} from '../data.test'; +import { DicomStructure, DicomStructureData } from '../Types'; + +describe('retrieveBulkData', () => { + + + it('should be able to add Bulk data uri to naturalized instances [addRetrieveBulkDataNaturalized]', () => { + const result: DicomStructureData = naturalizedInstances.map( + instance => addRetrieveBulkDataNaturalized( + instance, + client, + dicomWebConfig, + ) + ); + expect(JSON.stringify(result)).toStrictEqual(JSON.stringify(expectedNaturalizedInstances)); + expect(result.pop().PixelData.BulkDataURI).toStrictEqual(bulkDataURIExample); + }); + + + it('should be able to add Bulk data uri to naturalized instances [addRetrieveBulkData]', () => { + const result = naturalizedInstances.map( + instance => addRetrieveBulkData( + instance, + client, + dicomWebConfig, + ) + ); + expect(JSON.stringify(result)).toStrictEqual(JSON.stringify(expectedNaturalizedInstances)); + expect(result.pop().PixelData.BulkDataURI).toStrictEqual(bulkDataURIExample); + }); +}); diff --git a/extensions/default/src/DataSources/utils/wado/retrieveBulkData.ts b/extensions/default/src/DataSources/utils/wado/retrieveBulkData.ts new file mode 100644 index 00000000000..a8c7729e3a0 --- /dev/null +++ b/extensions/default/src/DataSources/utils/wado/retrieveBulkData.ts @@ -0,0 +1,95 @@ + +import dcmjs from 'dcmjs'; +import { fixBulkDataURI } from '../fixBulkDataURI'; + +const { DicomMetaDictionary } = dcmjs.data; +const { naturalizeDataset } = DicomMetaDictionary; + +import DICOMwebClient from 'dicomweb-client/types/api'; +import {DicomWebConfig} from '../utils/dicomWebConfig'; +import {DicomStructure} from '../utils/Types'; + +/** + * Adds the retrieve bulkdata function to naturalized DICOM data. + * This is done recursively, for sub-sequences. + */ +export function addRetrieveBulkDataNaturalized( + naturalized: DicomStructure, + client: DICOMwebClient, + config: DicomWebConfig, +): DicomStructure { + for (const key of Object.keys(naturalized)) { + const value = naturalized[key]; + + if (Array.isArray(value) && typeof value[0] === 'object') { + // Fix recursive values + const validValues = value.filter(Boolean); + validValues.forEach(child => addRetrieveBulkDataNaturalized(child, client, config)); + continue; + } + + // 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) { + // handle the scenarios where bulkDataURI is relative path + fixBulkDataURI(value, naturalized, config); + // Provide a method to fetch bulkdata + value.retrieveBulkData = retrieveBulkData.bind(client, value); + } + } + return naturalized; +} + +/** + * A bindable function that retrieves the bulk data against this as the + * dicomweb client, and on the given value element. + * + * @param value - a bind value that stores the retrieve value to short circuit the + * next retrieve instance. + * @param options - to allow specifying the content type. + */ +export function retrieveBulkData( + value, + options = { mediaType: undefined } +) { + const { mediaType } = options; + const useOptions = { + // The bulkdata fetches work with either multipart or + // singlepart, so set multipart to false to let the server + // decide which type to respond with. + multipart: false, + BulkDataURI: value.BulkDataURI, + mediaTypes: mediaType ? [{ mediaType }, { mediaType: 'application/octet-stream' }] : undefined, + ...options, + }; + return this.retrieveBulkData(useOptions).then(val => { + // There are DICOM PDF cases where the first ArrayBuffer in the array is + // the bulk data and DICOM video cases where the second ArrayBuffer is + // the bulk data. Here we play it safe and do a find. + const ret = + (val instanceof Array && val.find(arrayBuffer => arrayBuffer?.byteLength)) || undefined; + value.Value = ret; + return ret; + }); +} + +/** + * naturalizes the dataset, and adds a retrieve bulkdata method + * to any values containing BulkDataURI. + * @param {DicomStructure} naturalized + * @param {DICOMwebClient} client + * @param {DicomWebConfig} config + * @returns naturalized dataset, with retrieveBulkData methods + */ +export function addRetrieveBulkData( + naturalized: DicomStructure, + client: DICOMwebClient, + config: DicomWebConfig): DicomStructure +{ + // if we know the server doesn't use bulkDataURI, then don't + if (!config.bulkDataURI?.enabled) { + return naturalized; + } + + return addRetrieveBulkDataNaturalized(naturalized, client, config); +} \ No newline at end of file diff --git a/extensions/default/src/DataSources/utils/wado/retrieveInstanceMetadata.ts b/extensions/default/src/DataSources/utils/wado/retrieveInstanceMetadata.ts new file mode 100644 index 00000000000..295d9b36b59 --- /dev/null +++ b/extensions/default/src/DataSources/utils/wado/retrieveInstanceMetadata.ts @@ -0,0 +1,37 @@ +import { retrieveStudyMetadata } from './retrieveStudyMetadata'; +import DICOMwebClient from 'dicomweb-client/types/api'; +import { DicomWebConfig, BulkDataURIConfig } from '../dicomWebConfig'; + +/** + * Retrieval of instance metadata updated to allow optional passthrough of SOPInstanceUID so that we can + * retrieve individual slices instead of the full series or study. + * + * @param wadoDicomWebClient client needed to execute retrieval + * @param StudyInstanceUID + * @param enableStudyLazyLoad + * @param instanceMeta Instance metadata from which we can obtain the series uid and instance uid if available + * @param sortCriteria + * @param sortFunction + * @param dicomWebConfig system configuration structure. + */ +export async function retrieveInstanceMetadata( + wadoDicomWebClient: DICOMwebClient, + StudyInstanceUID: string, + enableStudyLazyLoad: boolean, + instanceMeta: any, + sortCriteria, + sortFunction: Function, + dicomWebConfig: DicomWebConfig +) { + const seriesInstanceUID = instanceMeta.seriesInstanceUID; + const sopInstanceUID = instanceMeta.sopInstanceUID; + return retrieveStudyMetadata( + wadoDicomWebClient, + StudyInstanceUID, + enableStudyLazyLoad, + {seriesInstanceUID, sopInstanceUID}, + sortCriteria, + sortFunction, + dicomWebConfig + ); +} \ No newline at end of file diff --git a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadata.js b/extensions/default/src/DataSources/utils/wado/retrieveMetadata.js similarity index 100% rename from extensions/default/src/DicomWebDataSource/wado/retrieveMetadata.js rename to extensions/default/src/DataSources/utils/wado/retrieveMetadata.js diff --git a/extensions/default/src/DicomWebDataSource/utils/retrieveMetadataFiltered.js b/extensions/default/src/DataSources/utils/wado/retrieveMetadataFiltered.js similarity index 96% rename from extensions/default/src/DicomWebDataSource/utils/retrieveMetadataFiltered.js rename to extensions/default/src/DataSources/utils/wado/retrieveMetadataFiltered.js index 5adc635b5a7..7556cd017ab 100644 --- a/extensions/default/src/DicomWebDataSource/utils/retrieveMetadataFiltered.js +++ b/extensions/default/src/DataSources/utils/wado/retrieveMetadataFiltered.js @@ -21,7 +21,7 @@ function retrieveMetadataFiltered( sortCriteria, sortFunction ) { - const { seriesInstanceUID } = filters; + const { seriesInstanceUID, sopInstanceUID } = filters; return new Promise((resolve, reject) => { const promises = seriesInstanceUID.map(uid => { diff --git a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoader.js b/extensions/default/src/DataSources/utils/wado/retrieveMetadataLoader.js similarity index 100% rename from extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoader.js rename to extensions/default/src/DataSources/utils/wado/retrieveMetadataLoader.js diff --git a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderAsync.js b/extensions/default/src/DataSources/utils/wado/retrieveMetadataLoaderAsync.js similarity index 71% rename from extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderAsync.js rename to extensions/default/src/DataSources/utils/wado/retrieveMetadataLoaderAsync.js index 0c9a656039f..8516a273185 100644 --- a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderAsync.js +++ b/extensions/default/src/DataSources/utils/wado/retrieveMetadataLoaderAsync.js @@ -1,58 +1,12 @@ import dcmjs from 'dcmjs'; import { sortStudySeries } from '@ohif/core/src/utils/sortStudy'; import RetrieveMetadataLoader from './retrieveMetadataLoader'; +import {DeferredPromise} from '../Types' // Series Date, Series Time, Series Description and Series Number to be included // in the series metadata query result const includeField = ['00080021', '00080031', '0008103E', '00200011'].join(','); -export class DeferredPromise { - metadata = undefined; - processFunction = undefined; - internalPromise = undefined; - thenFunction = undefined; - rejectFunction = undefined; - - setMetadata(metadata) { - this.metadata = metadata; - } - setProcessFunction(func) { - this.processFunction = func; - } - getPromise() { - return this.start(); - } - start() { - if (this.internalPromise) { - return this.internalPromise; - } - this.internalPromise = this.processFunction(); - // in case then and reject functions called before start - if (this.thenFunction) { - this.then(this.thenFunction); - this.thenFunction = undefined; - } - if (this.rejectFunction) { - this.reject(this.rejectFunction); - this.rejectFunction = undefined; - } - return this.internalPromise; - } - then(func) { - if (this.internalPromise) { - return this.internalPromise.then(func); - } else { - this.thenFunction = func; - } - } - reject(func) { - if (this.internalPromise) { - return this.internalPromise.reject(func); - } else { - this.rejectFunction = func; - } - } -} /** * Creates an immutable series loader object which loads each series sequentially using the iterator interface. * @@ -69,15 +23,13 @@ function makeSeriesAsyncLoader(client, studyInstanceUID, seriesInstanceUIDList) }, next() { const { seriesInstanceUID, metadata } = seriesInstanceUIDList.shift(); - const promise = new DeferredPromise(); - promise.setMetadata(metadata); - promise.setProcessFunction(() => { + + return new DeferredPromise(metadata, () => { return client.retrieveSeriesMetadata({ studyInstanceUID, seriesInstanceUID, }); }); - return promise; }, }); } @@ -94,7 +46,7 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader */ *getPreLoaders() { const preLoaders = []; - const { studyInstanceUID, filters: { seriesInstanceUID } = {}, client } = this; + const { studyInstanceUID, filters: { seriesInstanceUID, sopInstanceUID } = {}, client } = this; // asking to include Series Date, Series Time, Series Description // and Series Number in the series metadata returned to better sort series diff --git a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderSync.js b/extensions/default/src/DataSources/utils/wado/retrieveMetadataLoaderSync.js similarity index 72% rename from extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderSync.js rename to extensions/default/src/DataSources/utils/wado/retrieveMetadataLoaderSync.js index 01d83e025bc..3d150b847fe 100644 --- a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderSync.js +++ b/extensions/default/src/DataSources/utils/wado/retrieveMetadataLoaderSync.js @@ -10,29 +10,22 @@ import RetrieveMetadataLoader from './retrieveMetadataLoader'; * I.e Retrieve metadata using all loaders possibilities. */ export default class RetrieveMetadataLoaderSync extends RetrieveMetadataLoader { - getOptions() { - const { studyInstanceUID, filters } = this; - - const options = { - studyInstanceUID, - }; - - const { seriesInstanceUID } = filters; - if (seriesInstanceUID) { - options['seriesInstanceUID'] = seriesInstanceUID; - } - - return options; - } - /** * @returns {Array} Array of loaders. To be consumed as queue */ *getLoaders() { const loaders = []; - const { studyInstanceUID, filters: { seriesInstanceUID } = {}, client } = this; + const { studyInstanceUID, filters: { seriesInstanceUID, sopInstanceUID } = {}, client } = this; - if (seriesInstanceUID) { + if(seriesInstanceUID && sopInstanceUID) { + loaders.push( + client.retrieveInstanceMetadata.bind(client, { + studyInstanceUID, + seriesInstanceUID, + sopInstanceUID + }) + ); + }else if (seriesInstanceUID) { loaders.push( client.retrieveSeriesMetadata.bind(client, { studyInstanceUID, diff --git a/extensions/default/src/DicomWebDataSource/retrieveStudyMetadata.js b/extensions/default/src/DataSources/utils/wado/retrieveStudyMetadata.js similarity index 86% rename from extensions/default/src/DicomWebDataSource/retrieveStudyMetadata.js rename to extensions/default/src/DataSources/utils/wado/retrieveStudyMetadata.js index 9c3c5901fdc..faabd298ddb 100644 --- a/extensions/default/src/DicomWebDataSource/retrieveStudyMetadata.js +++ b/extensions/default/src/DataSources/utils/wado/retrieveStudyMetadata.js @@ -1,5 +1,5 @@ -import retrieveMetadataFiltered from './utils/retrieveMetadataFiltered.js'; -import RetrieveMetadata from './wado/retrieveMetadata.js'; +import retrieveMetadataFiltered from './retrieveMetadataFiltered.js'; +import RetrieveMetadata from './retrieveMetadata.js'; const moduleName = 'RetrieveStudyMetadata'; // Cache for promises. Prevents unnecessary subsequent calls to the server @@ -38,7 +38,9 @@ export function retrieveStudyMetadata( throw new Error(`${moduleName}: Required 'StudyInstanceUID' parameter not provided.`); } - const promiseId = `${dicomWebConfig.name}:${StudyInstanceUID}`; + const seriesInstanceUID = filters && filters.seriesInstanceUID ? filters.seriesInstanceUID : undefined; + const sopInstanceUID = filters && filters.sopInstanceUID ? filters.sopInstanceUID : undefined; + const promiseId = `${dicomWebConfig.name}:${StudyInstanceUID}:${seriesInstanceUID}:${sopInstanceUID}`; // Already waiting on result? Return cached promise if (StudyMetaDataPromises.has(promiseId)) { diff --git a/extensions/default/src/DicomWebDataSource/utils/index.ts b/extensions/default/src/DicomWebDataSource/utils/index.ts deleted file mode 100644 index 3c34ea1a413..00000000000 --- a/extensions/default/src/DicomWebDataSource/utils/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { fixBulkDataURI } from './fixBulkDataURI'; -import { - cleanDenaturalizedDataset, - transferDenaturalizedDataset, -} from './cleanDenaturalizedDataset'; - -export { fixMultiValueKeys } from './fixMultiValueKeys'; - -export { fixBulkDataURI, cleanDenaturalizedDataset, transferDenaturalizedDataset }; diff --git a/extensions/default/src/getDataSourcesModule.js b/extensions/default/src/getDataSourcesModule.js index 85ae2391639..32e08966355 100644 --- a/extensions/default/src/getDataSourcesModule.js +++ b/extensions/default/src/getDataSourcesModule.js @@ -2,11 +2,12 @@ // TODO: Use constructor to create an instance of IWebClientApi // TODO: Use existing DICOMWeb configuration (previously, appConfig, to configure instance) -import { createDicomWebApi } from './DicomWebDataSource/index'; -import { createDicomJSONApi } from './DicomJSONDataSource/index'; -import { createDicomLocalApi } from './DicomLocalDataSource/index'; -import { createDicomWebProxyApi } from './DicomWebProxyDataSource/index'; -import { createMergeDataSourceApi } from './MergeDataSource/index'; +import { createDicomWebApi } from './DataSources/DicomWebDataSource/index'; +import { createDicomWebMinimalApi } from './DataSources/DicomWebMinimalDataSource/index'; +import { createDicomJSONApi } from './DataSources/DicomJSONDataSource/index'; +import { createDicomLocalApi } from './DataSources/DicomLocalDataSource/index'; +import { createDicomWebProxyApi } from './DataSources/DicomWebProxyDataSource/index'; +import { createMergeDataSourceApi } from './DataSources/MergeDataSource/index'; /** * @@ -18,6 +19,11 @@ function getDataSourcesModule() { type: 'webApi', createDataSource: createDicomWebApi, }, + { + name: 'dicomweb_minimal', + type: 'webApi', + createDataSource: createDicomWebMinimalApi, + }, { name: 'dicomwebproxy', type: 'webApi', diff --git a/extensions/default/src/index.ts b/extensions/default/src/index.ts index 133d5878ee8..90c443b0b1d 100644 --- a/extensions/default/src/index.ts +++ b/extensions/default/src/index.ts @@ -15,10 +15,10 @@ import preRegistration from './init'; import { createReportDialogPrompt } from './Panels'; import { ContextMenuController, CustomizableContextMenuTypes } from './CustomizableContextMenu'; -import * as dicomWebUtils from './DicomWebDataSource/utils'; +import * as dicomWebUtils from './DataSources/utils'; import createReportAsync from './Actions/createReportAsync'; -import StaticWadoClient from './DicomWebDataSource/utils/StaticWadoClient'; -import { cleanDenaturalizedDataset } from './DicomWebDataSource/utils'; +import { StaticWadoClient } from './DataSources/utils'; +import { cleanDenaturalizedDataset } from './DataSources/utils'; import { useViewportsByPositionStore } from './stores/useViewportsByPositionStore'; import { useViewportGridStore } from './stores/useViewportGridStore'; import { useUIStateStore } from './stores/useUIStateStore'; diff --git a/platform/app/src/routes/Mode/defaultRouteInit.ts b/platform/app/src/routes/Mode/defaultRouteInit.ts index fceb7573c9d..487c08f0eea 100644 --- a/platform/app/src/routes/Mode/defaultRouteInit.ts +++ b/platform/app/src/routes/Mode/defaultRouteInit.ts @@ -51,7 +51,6 @@ export async function defaultRouteInit( DicomMetadataStore.EVENTS.INSTANCES_ADDED, function ({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) { const seriesMetadata = DicomMetadataStore.getSeries(StudyInstanceUID, SeriesInstanceUID); - // checks if the series filter was used, if it exists const seriesInstanceUIDs = filters?.seriesInstanceUID; if ( @@ -79,6 +78,7 @@ export async function defaultRouteInit( log.time(Enums.TimingEnum.STUDY_TO_FIRST_IMAGE); const allRetrieves = studyInstanceUIDs.map(StudyInstanceUID => + //dataSource.query.series.list(StudyInstanceUID, filters) dataSource.retrieve.series.metadata({ StudyInstanceUID, filters, @@ -106,7 +106,7 @@ export async function defaultRouteInit( displaySetFromUrl = true; } - await Promise.allSettled(allRetrieves).then(async promises => { + await Promise.allSettled(allRetrieves).then(async (promises: Promise[]) => { log.timeEnd(Enums.TimingEnum.STUDY_TO_DISPLAY_SETS); log.time(Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE); log.time(Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES); @@ -114,10 +114,6 @@ export async function defaultRouteInit( const allPromises = []; const remainingPromises = []; - function startRemainingPromises(remainingPromises) { - remainingPromises.forEach(p => p.forEach(p => p.start())); - } - promises.forEach(promise => { const retrieveSeriesMetadataPromise = promise.value; if (!Array.isArray(retrieveSeriesMetadataPromise)) { @@ -125,24 +121,19 @@ export async function defaultRouteInit( } if (displaySetFromUrl) { - const requiredSeriesPromises = retrieveSeriesMetadataPromise.map(promise => - promise.start() - ); - allPromises.push(Promise.allSettled(requiredSeriesPromises)); + allPromises.push(Promise.allSettled(retrieveSeriesMetadataPromise)); } else { const { requiredSeries, remaining } = hangingProtocolService.filterSeriesRequiredForRun( hangingProtocolId, retrieveSeriesMetadataPromise ); - const requiredSeriesPromises = requiredSeries.map(promise => promise.start()); - allPromises.push(Promise.allSettled(requiredSeriesPromises)); + + allPromises.push(Promise.allSettled(requiredSeries)); remainingPromises.push(remaining); } }); await Promise.allSettled(allPromises).then(applyHangingProtocol); - startRemainingPromises(remainingPromises); - applyHangingProtocol(); }); return unsubscriptions; diff --git a/platform/core/src/DICOMWeb/getAuthorizationHeader.test.js b/platform/core/src/DICOMWeb/getAuthorizationHeader.test.ts similarity index 77% rename from platform/core/src/DICOMWeb/getAuthorizationHeader.test.js rename to platform/core/src/DICOMWeb/getAuthorizationHeader.test.ts index 9bf0b4636b5..2ac4c334a4d 100644 --- a/platform/core/src/DICOMWeb/getAuthorizationHeader.test.js +++ b/platform/core/src/DICOMWeb/getAuthorizationHeader.test.ts @@ -1,7 +1,8 @@ import getAuthorizationHeader from './getAuthorizationHeader'; import user from './../user'; +import { HeadersInterface, RequestOptions } from '../types/RequestHeaders'; -jest.mock('./../user.js'); +jest.mock('../user'); describe('getAuthorizationHeader', () => { it('should return a HTTP Basic Auth when server contains requestOptions.auth', () => { @@ -15,7 +16,7 @@ describe('getAuthorizationHeader', () => { Authorization: `Basic ${btoa(validServer.requestOptions.auth)}`, }; - const authentication = getAuthorizationHeader(validServer); + const authentication: HeadersInterface = getAuthorizationHeader(validServer); expect(authentication).toEqual(expectedAuthorizationHeader); }); @@ -31,13 +32,13 @@ describe('getAuthorizationHeader', () => { Authorization: `Basic ${btoa(validServerWithoutPassword.requestOptions.auth)}`, }; - const authentication = getAuthorizationHeader(validServerWithoutPassword); + const authentication: HeadersInterface = getAuthorizationHeader(validServerWithoutPassword); expect(authentication).toEqual(expectedAuthorizationHeader); }); it('should return a HTTP Basic Auth when server contains requestOptions.auth custom function', () => { - const validServerCustomAuth = { + const validServerCustomAuth: RequestOptions = { requestOptions: { auth: options => `Basic ${options.token}`, token: 'ZHVtbXlfdXNlcjpkdW1teV9wYXNzd29yZA==', @@ -48,13 +49,13 @@ describe('getAuthorizationHeader', () => { Authorization: `Basic ${validServerCustomAuth.requestOptions.token}`, }; - const authentication = getAuthorizationHeader(validServerCustomAuth); + const authentication: HeadersInterface = getAuthorizationHeader(validServerCustomAuth); expect(authentication).toEqual(expectedAuthorizationHeader); }); it('should return an empty object when there is no either server.requestOptions.auth or accessToken', () => { - const authentication = getAuthorizationHeader({}); + const authentication: HeadersInterface = getAuthorizationHeader({}); expect(authentication).toEqual({}); }); @@ -62,7 +63,7 @@ describe('getAuthorizationHeader', () => { it('should return an Authorization with accessToken when server is not defined and there is an accessToken', () => { user.getAccessToken.mockImplementationOnce(() => 'MOCKED_TOKEN'); - const authentication = getAuthorizationHeader({}, user); + const authentication: HeadersInterface = getAuthorizationHeader({}, user); const expectedHeaderBasedOnUserAccessToken = { Authorization: 'Bearer MOCKED_TOKEN', }; diff --git a/platform/core/src/DICOMWeb/getAuthorizationHeader.js b/platform/core/src/DICOMWeb/getAuthorizationHeader.ts similarity index 64% rename from platform/core/src/DICOMWeb/getAuthorizationHeader.js rename to platform/core/src/DICOMWeb/getAuthorizationHeader.ts index 4d996bb97d6..fbd80e1377d 100644 --- a/platform/core/src/DICOMWeb/getAuthorizationHeader.js +++ b/platform/core/src/DICOMWeb/getAuthorizationHeader.ts @@ -1,17 +1,23 @@ import 'isomorphic-base64'; -import user from '../user'; +import {UserAccountInterface} from '../user'; +import { HeadersInterface, RequestOptions } from '../types/RequestHeaders'; /** * Returns the Authorization header as part of an Object. * * @export * @param {Object} [server={}] - * @param {Object} [server.requestOptions] - * @param {string|function} [server.requestOptions.auth] + * @param {Object} [requestOptions] + * @param {string|function} [requestOptions.auth] + * @param {Object} [user] + * @param {function} [user.getAccessToken] * @returns {Object} { Authorization } */ -export default function getAuthorizationHeader({ requestOptions } = {}, user) { - const headers = {}; +export default function getAuthorizationHeader( + {requestOptions}: RequestOptions, + user: UserAccountInterface = {}): HeadersInterface +{ + const headers: HeadersInterface = {}; // Check for OHIF.user since this can also be run on the server const accessToken = user && user.getAccessToken && user.getAccessToken(); diff --git a/platform/core/src/DICOMWeb/index.js b/platform/core/src/DICOMWeb/index.js index 0775d927492..f8d7bf23b9b 100644 --- a/platform/core/src/DICOMWeb/index.js +++ b/platform/core/src/DICOMWeb/index.js @@ -1,5 +1,5 @@ import getAttribute from './getAttribute.js'; -import getAuthorizationHeader from './getAuthorizationHeader.js'; +import getAuthorizationHeader from './getAuthorizationHeader'; import getModalities from './getModalities.js'; import getName from './getName.js'; import getNumber from './getNumber.js'; diff --git a/platform/core/src/extensions/ExtensionManager.ts b/platform/core/src/extensions/ExtensionManager.ts index 677f8fe68ab..caf52199f09 100644 --- a/platform/core/src/extensions/ExtensionManager.ts +++ b/platform/core/src/extensions/ExtensionManager.ts @@ -97,7 +97,7 @@ export default class ExtensionManager extends PubSubService { appConfig = {}, }: ExtensionConstructor) { super(ExtensionManager.EVENTS); - this.modules = {}; + this.modules = {}; // initializes the module map to an empty object this.registeredExtensionIds = []; this.moduleTypeNames = Object.values(MODULE_TYPES); // @@ -107,6 +107,7 @@ export default class ExtensionManager extends PubSubService { this._hotkeysManager = hotkeysManager; this._appConfig = appConfig; + // Somehow this.modules is populated here... this.modulesMap = {}; this.moduleTypeNames.forEach(moduleType => { this.modules[moduleType] = []; @@ -594,6 +595,14 @@ export default class ExtensionManager extends PubSubService { this.dataSourceMap[dataSourceDef.sourceName] = [dataSourceInstance]; } + /** + * Initializes the requested DataSource module. + * If the namespace key matches any of the extension namespace keys in dataSources, that module + * instance gets added. + * @param extensionModule + * @param extensionId + * @param dataSources + */ _initDataSourcesModule( extensionModule, extensionId, diff --git a/platform/core/src/index.ts b/platform/core/src/index.ts index 134f471d25f..91c80df56c7 100644 --- a/platform/core/src/index.ts +++ b/platform/core/src/index.ts @@ -9,7 +9,7 @@ import errorHandler from './errorHandler.js'; import log from './log.js'; import object from './object.js'; import string from './string.js'; -import user from './user.js'; +import user from './user'; import utils from './utils'; import defaults from './defaults'; import * as Types from './types'; diff --git a/platform/core/src/types/RequestHeaders.ts b/platform/core/src/types/RequestHeaders.ts new file mode 100644 index 00000000000..2ed11722b36 --- /dev/null +++ b/platform/core/src/types/RequestHeaders.ts @@ -0,0 +1,35 @@ +/** + * Interface to clearly present the expected fields to linters when building a request header. + */ +export interface HeadersInterface { + /** + * Request Accept options. For example, + * `['multipart/related; type=application/octet-stream; transfer-syntax=1.2.840.10008.1.2.1.99',]`. + * + * Defines to the server the formats it can use to deliver data to us. + */ + Accept?: string[]; + /** + * Request Authorization field. It can be overridden with the `requestOptions.auth` config item. + * Contains the authorization credentials or tokens necessary to authorize the request with the + * server. + */ + Authorization?: string; +} + +/** + * Interface to clearly present the expected fields to linters when passing the configuration's + * requestOptions struct. + */ +export interface RequestOptions { + requestOptions?: { + /** + * Authentication options to include. Can be a function. + */ + auth?: Function | string; + /** + * Authentication token. Satisfies the test requirement? + */ + token?: string; + } +} diff --git a/platform/core/src/user.js b/platform/core/src/user.js deleted file mode 100644 index 9e5df8d0690..00000000000 --- a/platform/core/src/user.js +++ /dev/null @@ -1,13 +0,0 @@ -// These should be overridden by the implementation -let user = { - userLoggedIn: () => false, - getUserId: () => null, - getName: () => null, - getAccessToken: () => null, - login: () => new Promise((resolve, reject) => reject()), - logout: () => new Promise((resolve, reject) => reject()), - getData: key => null, - setData: (key, value) => null, -}; - -export default user; diff --git a/platform/core/src/user.ts b/platform/core/src/user.ts new file mode 100644 index 00000000000..f7a0da39a76 --- /dev/null +++ b/platform/core/src/user.ts @@ -0,0 +1,28 @@ +// These should be overridden by the implementation +let user = { + userLoggedIn: (): boolean => false, + getUserId: () => null, + getName: () => null, + getAccessToken: () => null, + login: () => new Promise((resolve, reject) => reject()), + logout: () => new Promise((resolve, reject) => reject()), + getData: key => null, + setData: (key, value) => null, +}; + +/** + * Interface to clearly present the expected fields to linters when passing the user account + * struct. + */ +export interface UserAccountInterface { + userLoggedIn?: () => boolean; + getUserId?: () => null; + getName?: () => null; + getAccessToken?: () => null; + login?: () => Promise; + logout?: () => Promise; + getData?: (key: any) => null; + setData?: (key: any, value: any) => null; +} + +export default user; diff --git a/platform/core/src/utils/generateAcceptHeader.ts b/platform/core/src/utils/generateAcceptHeader.ts index c384411a3f2..c20778c4fb3 100644 --- a/platform/core/src/utils/generateAcceptHeader.ts +++ b/platform/core/src/utils/generateAcceptHeader.ts @@ -1,5 +1,5 @@ const generateAcceptHeader = ( - configAcceptHeader = [], + configAcceptHeader: string[] = [], requestTransferSyntaxUID = '*', //default to accept all transfer syntax omitQuotationForMultipartRequest = false ): string[] => {