diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index f5e300647e..1d608c16a2 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -44,29 +44,43 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { dicomWebConfig, userAuthenticationService, }); - // here clientManager returns, if any, the reject function of the first - // client that supports it. This server should be the first main server - implementation.clientCanReject = clientManager.clientCanReject(); + }, + /** + * returns a boolean indicating if a client has reject capabilities + * @param {*} clientName + * @returns {boolean} + */ + clientCanReject: clientName => { + return clientManager.clientCanReject(clientName); + }, + /** + * implements the series rejection. Only dcm4chee series rejection is + * supported for now + */ + reject: { + series: (studyInstanceUID, seriesInstanceUID, clientName) => { + const rejectObject = clientManager.getClientReject(clientName); + if (rejectObject) { + return rejectObject.series(studyInstanceUID, seriesInstanceUID); + } + }, }, query: { studies: { mapParams: mapParams.bind(), search: async function (origParams) { - clientManager.setQidoHeaders(); - let results = []; - const studyInstanceUIDs = {}; + const clients = clientManager.getClientsForQidoRequests(); // concatenate series metadata from all servers - const clients = clientManager.getClients(); - for (let i = 0; i < clients.length; i++) { + const clientResultsPromises = clients.map(client => { const { studyInstanceUid, seriesInstanceUid, ...mappedParams } = mapParams(origParams, { - supportsFuzzyMatching: clients[i].supportsFuzzyMatching, - supportsWildcard: clients[i].supportsWildcard, + supportsFuzzyMatching: client.supportsFuzzyMatching, + supportsWildcard: client.supportsWildcard, }) || {}; let clientResults; try { - clientResults = await qidoSearch( - clients[i].qidoDicomWebClient, + clientResults = qidoSearch( + client.qidoDicomWebClient, undefined, undefined, mappedParams @@ -74,31 +88,25 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { } catch { clientResults = []; } + return clientResults; + }); + await Promise.allSettled(clientResultsPromises); + + let results = []; + const studyInstanceUIDs = {}; + // here remove duplicate studies + for (let i = 0; i < clientResultsPromises.length; i++) { + let clientResults; + try { + clientResults = await clientResultsPromises[i]; + } catch { + clientResults = []; + } for (let j = 0; j < clientResults.length; j++) { const studyInstanceUID = clientResults[j][STUDY_INSTANCE_UID].Value[0]; if (!(studyInstanceUID in studyInstanceUIDs)) { studyInstanceUIDs[studyInstanceUID] = clientResults[j]; results.push(clientResults[j]); - } else { - // updates number of series in study - if ( - studyInstanceUIDs[studyInstanceUID][NUMBER_STUDY_SERIES] && - clientResults[j][NUMBER_STUDY_SERIES] - ) { - studyInstanceUIDs[studyInstanceUID][NUMBER_STUDY_SERIES].Value[0] = - studyInstanceUIDs[studyInstanceUID][NUMBER_STUDY_SERIES].Value[0] + - clientResults[j][NUMBER_STUDY_SERIES].Value[0]; - } - - // updates number of instances in study - if ( - studyInstanceUIDs[studyInstanceUID][NUMBER_STUDY_INSTANCES] && - clientResults[j][NUMBER_STUDY_INSTANCES] - ) { - studyInstanceUIDs[studyInstanceUID][NUMBER_STUDY_INSTANCES].Value[0] = - studyInstanceUIDs[studyInstanceUID][NUMBER_STUDY_INSTANCES].Value[0] + - clientResults[j][NUMBER_STUDY_INSTANCES].Value[0]; - } } } } @@ -108,11 +116,10 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { }, series: { search: async function (studyInstanceUid) { - clientManager.setQidoHeaders(); + const clients = clientManager.getClientsForQidoRequests(); let results = []; const seriesInstanceUIDs = []; // concatenate series metadata from all servers - const clients = clientManager.getClients(); for (let i = 0; i < clients.length; i++) { let clientResults; try { @@ -120,8 +127,9 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { } catch { clientResults = []; } + // remove duplicate series in results for (let j = 0; j < clientResults.length; j++) { - const seriesInstanceUID = clientResults[j]['0020000E'].Value[0]; + const seriesInstanceUID = clientResults[j][SERIES_INSTANCE_UID].Value[0]; if (!seriesInstanceUIDs.includes(seriesInstanceUID)) { seriesInstanceUIDs.push(seriesInstanceUID); results.push(clientResults[j]); @@ -133,9 +141,8 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { }, instances: { search: (studyInstanceUid, queryParameters) => { - clientManager.setQidoHeaders(); + const clients = clientManager.getClientsForQidoRequests(); // concatenate instance metadata from all servers - const clients = clientManager.getClients(); for (let i = 0; i < clients.length; i++) { qidoSearch.call( undefined, @@ -157,11 +164,11 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { * or is already retrieved, or a promise to a URL for such use if a BulkDataURI */ directURL: params => { - const clientName = params?.instance?.clientName; + const client = clientManager.getClient(params?.instance?.clientName); return getDirectURL( { - wadoRoot: clientManager.getClient(clientName).wadoRoot, - singlepart: clientManager.getClient(clientName).singlepart, + wadoRoot: client.wadoRoot, + singlepart: client.singlepart, }, params ); @@ -218,12 +225,12 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { dicom: async (dataset, request) => { clientManager.setAuthorizationHeadersForWADO(); const clientName = dataset?.clientName; + let options; if (dataset instanceof ArrayBuffer) { - const options = { + options = { datasets: [dataset], request, }; - await clientManager.getWadoClient(clientName).storeInstances(options); } else { const meta = { FileMetaInformationVersion: dataset._meta?.FileMetaInformationVersion?.Value, @@ -241,13 +248,12 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { const part10Buffer = dicomDict.write(); - const options = { + options = { datasets: [part10Buffer], request, }; - - await clientManager.getWadoClient(clientName).storeInstances(options); } + await clientManager.getWadoClient(clientName).storeInstances(options); }, }, @@ -259,12 +265,11 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { madeInClient ) => { const enableStudyLazyLoad = false; - clientManager.setWadoHeaders(); + const clients = clientManager.getClientsForWadoRequests(); const naturalizedInstancesMetadata = []; const seriesConcatenated = []; // search and retrieve in all servers - const clients = clientManager.getClients(); for (let i = 0; i < clients.length; i++) { const data = await retrieveStudyMetadata( clients[i].wadoDicomWebClient, @@ -355,9 +360,8 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { madeInClient = false ) => { const enableStudyLazyLoad = true; - clientManager.setWadoHeaders(); + const clients = clientManager.getClientsForWadoRequests(); // Get Series - const clients = clientManager.getClients(); let seriesSummaryMetadata = []; let seriesPromises = []; const seriesClientsMapping = {}; @@ -379,7 +383,9 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { clientSeriesPromises = []; } - // create a mapping between SeriesInstanceUID <--> clientName + // create a mapping between SeriesInstanceUID <--> clientName, for two reasons: + // 1 - remove duplicates in series metadata + // 2 - associate each instance in a series with the name of the client it was retrieved for (let j = 0; j < clientSeriesSummaryMetadata.length; j++) { if (!seriesClientsMapping[clientSeriesSummaryMetadata[j].SeriesInstanceUID]) { seriesClientsMapping[clientSeriesSummaryMetadata[j].SeriesInstanceUID] = @@ -400,9 +406,10 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { const addRetrieveBulkData = instance => { const clientName = instance?.clientName; const naturalized = naturalizeDataset(instance); + const client = clientManager.getClient(clientName); // if we know the server doesn't use bulkDataURI, then don't - if (!clientManager.getClient(clientName).bulkDataURI?.enabled) { + if (!client.bulkDataURI?.enabled) { return naturalized; } @@ -415,7 +422,7 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { // Provide a method to fetch bulkdata value.retrieveBulkData = () => { // handle the scenarios where bulkDataURI is relative path - fixBulkDataURI(value, naturalized, clientManager.getClient(clientName)); + fixBulkDataURI(value, naturalized, client); const options = { // The bulkdata fetches work with either multipart or @@ -454,10 +461,13 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { const naturalizedInstances = instances.map(addRetrieveBulkData); // Adding instanceMetadata to OHIF MetadataProvider + const client = clientManager.getClient(clientName); + const wadoRoot = client.wadoRoot; + const wadoUri = client.wadoUri; naturalizedInstances.forEach((instance, index) => { // attach client specific information in each instance - instance.wadoRoot = clientManager.getClient(clientName).wadoRoot; - instance.wadoUri = clientManager.getClient(clientName).wadoUri; + instance.wadoRoot = wadoRoot; + instance.wadoUri = wadoUri; instance.clientName = clientName; const imageId = implementation.getImageIdsForInstance({ @@ -541,7 +551,7 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { return imageIds; }, getConfig() { - return clientManager?.getClient(); + return clientManager?.getConfig(); }, getStudyInstanceUIDs({ params, query }) { const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params; diff --git a/extensions/default/src/DicomWebDataSource/retrieveStudyMetadata.js b/extensions/default/src/DicomWebDataSource/retrieveStudyMetadata.js index 0e5adc2dbb..c5f2395d8e 100644 --- a/extensions/default/src/DicomWebDataSource/retrieveStudyMetadata.js +++ b/extensions/default/src/DicomWebDataSource/retrieveStudyMetadata.js @@ -7,12 +7,13 @@ const StudyMetaDataPromises = new Map(); /** * Retrieves study metadata * - * @param {Object} server Object with server configuration parameters + * @param {Object} dicomWebClient Object with server configuration parameters * @param {string} StudyInstanceUID The UID of the Study to be retrieved * @param {boolean} enabledStudyLazyLoad Whether the study metadata should be loaded asynchronously. - * @param {function} storeInstancesCallback A callback used to store the retrieved instance metadata. * @param {Object} [filters] - Object containing filters to be applied on retrieve metadata process * @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against + * @param {Object} sortCriteria - defines the sort criteria for series + * @param {function} sortFunction - defines a function to sort series. If defined, it has precedence over sortCriteria param * @returns {Promise} that will be resolved with the metadata or rejected with the error */ export function retrieveStudyMetadata( @@ -34,7 +35,9 @@ export function retrieveStudyMetadata( throw new Error(`${moduleName}: Required 'StudyInstanceUID' parameter not provided.`); } - // Already waiting on result? Return cached promise + // Already waiting on result? Return cached promise. The StudyMetaDataPromises + // key is a combination of the client name and StudyInstanceUID, because OHIF + // can retrieve dicom series belonging to a study from different servers. const studyRetrieveId = `${dicomWebClient.name}/${StudyInstanceUID}`; if (StudyMetaDataPromises.has(studyRetrieveId)) { return StudyMetaDataPromises.get(studyRetrieveId); diff --git a/extensions/default/src/DicomWebDataSource/utils/clientManager.ts b/extensions/default/src/DicomWebDataSource/utils/clientManager.ts index 0cfdb634ee..d4ed0493f8 100644 --- a/extensions/default/src/DicomWebDataSource/utils/clientManager.ts +++ b/extensions/default/src/DicomWebDataSource/utils/clientManager.ts @@ -3,6 +3,32 @@ import StaticWadoClient from './StaticWadoClient'; import dcm4cheeReject from '../dcm4cheeReject'; import { errorHandler, utils } from '@ohif/core'; +/** + * This object plays the central role in OHIF's multiple server handling ability. + * It stores all servers configurations and handles all request headers generations + */ +interface ConfigToAdd { + url: string; + qidoRoot: string; + wadoRoot: string; + staticWado: boolean; + singlepart: boolean; + name: string; + qidoConfig: { + url: string; + staticWado: boolean; + singlepart: boolean; + headers: object; + }; + wadoConfig: { + url: string; + staticWado: boolean; + singlepart: boolean; + headers: object; + }; + qidoDicomWebClient: api.DICOMwebClient | StaticWadoClient; + wadoDicomWebClient: api.DICOMwebClient | StaticWadoClient; +} export default class ClientManager { clients; userAuthenticationService; @@ -10,19 +36,16 @@ export default class ClientManager { constructor({ params, query, dicomWebConfig, userAuthenticationService }) { this.clients = []; this.userAuthenticationService = userAuthenticationService; - if (Array.isArray(dicomWebConfig)) { - dicomWebConfig.forEach(config => this.addConfiguration(params, query, config)); - } else { - this.addConfiguration(params, query, dicomWebConfig); - } + const configArray = Array.isArray(dicomWebConfig) ? dicomWebConfig : [dicomWebConfig]; + configArray.forEach(config => this.addConfiguration(params, query, config)); } /** - * Adds a dicomweb server configuration in the clients list + * Adds a client to client list given a dicomweb server configuration * @param configToAdd * @returns {void} */ - private _addConfiguration(configToAdd): void { + private addClient(configToAdd: ConfigToAdd): void { const config = Object.assign({}, configToAdd); config.qidoConfig = { url: config.qidoRoot, @@ -54,11 +77,12 @@ export default class ClientManager { } /** - * Adds a dicomweb server configuration in the clients list. This function - * could change the configuration by calling the onConfiguration, if defined - * @param params - * @param query - * @param config + * Process a dicomweb server configuration and add it to the clients list. + * This function could change the configuration by calling the onConfiguration, + * if defined + * @param {object} params key / pair mapping of the URL parameters + * @param {object} query URLSearchParams object generated for the URL + * @param {object} config client configuration * @returns {void} */ private addConfiguration(params, query, config): void { @@ -68,7 +92,7 @@ export default class ClientManager { query, }); } - this._addConfiguration(config); + this.addClient(config); } /** @@ -78,7 +102,7 @@ export default class ClientManager { private getAuthorizationHeader(): object { const xhrRequestHeaders = {}; const authHeaders = this.userAuthenticationService.getAuthorizationHeader(); - if (authHeaders && authHeaders.Authorization) { + if (authHeaders?.Authorization) { xhrRequestHeaders.Authorization = authHeaders.Authorization; } return xhrRequestHeaders; @@ -89,7 +113,7 @@ export default class ClientManager { * @param config * @returns {object} wado Headers */ - private generateWadoHeader(config): object { + private getWadoHeader(config): object { const authorizationHeader = this.getAuthorizationHeader(); //Generate accept header depending on config params const formattedAcceptHeader = utils.generateAcceptHeader( @@ -120,7 +144,7 @@ export default class ClientManager { */ public setWadoHeaders(): void { this.clients.forEach( - client => (client.wadoDicomWebClient.headers = this.generateWadoHeader(client)) + client => (client.wadoDicomWebClient.headers = this.getWadoHeader(client)) ); } @@ -135,23 +159,20 @@ export default class ClientManager { } /** - * Returns a function that returns if a client have reject abilities - * @returns {function} function that returns if a client can reject + * Returns a boolean indicating if a client have reject abilities + * @returns {boolean} client reject support */ - public clientCanReject() { - return name => { - const client = this.clients.find(client => client.name === name); - return client?.supportsReject; - }; + public clientCanReject(name) { + return this.getClient(name)?.supportsReject; } /** - * Returns reject function of a client + * Returns the reject function of a client * @param name - * @returns {function} reject function + * @returns {object} client reject object */ - public getClientReject(name) { - const client = this.clients.find(client => client.name === name); + public getClientRejectObject(name) { + const client = this.getClient(name); if (client?.supportsReject) { return dcm4cheeReject(client.wadoRoot); } @@ -162,15 +183,9 @@ export default class ClientManager { * @param name * @returns {object} qido client */ - public getQidoClient(name = undefined): object { - if (this.clients.length) { - if (name) { - const client = this.clients.find(client => client.name === name); - return client?.qidoDicomWebClient; - } else { - return this.clients[0].qidoDicomWebClient; - } - } + public getQidoClient(name?: string): object { + const client = this.getClient(name); + return client?.qidoDicomWebClient; } /** @@ -178,15 +193,9 @@ export default class ClientManager { * @param name * @returns {object} wado client */ - public getWadoClient(name = undefined): object { - if (this.clients.length) { - if (name) { - const client = this.clients.find(client => client.name === name); - return client?.wadoDicomWebClient; - } else { - return this.clients[0].wadoDicomWebClient; - } - } + public getWadoClient(name?: string): object { + const client = this.getClient(name); + return client?.wadoDicomWebClient; } /** @@ -194,15 +203,35 @@ export default class ClientManager { * @param name * @returns {object} client configuration */ - public getClient(name = undefined): object { - if (this.clients.length) { - if (name) { - const client = this.clients.find(client => client.name === name); - return client; - } else { - return this.clients[0]; - } - } + public getConfig(name?: string): object { + return name ? this.clients.find(client => client.name === name) : this.clients[0]; + } + + /** + * Returns the client configuration + * @param name + * @returns {object} client configuration + */ + private getClient(name?: string): object { + return name ? this.clients.find(client => client.name === name) : this.clients[0]; + } + + /** + * Gets the client list already setting the necessary wado headers + * @returns {Array} client list + */ + public getClientsForWadoRequests() { + this.setWadoHeaders(); + return this.getClients(); + } + + /** + * Gets the client list already setting the necessary qido headers + * @returns {Array} client list + */ + public getClientsForQidoRequests() { + this.setQidoHeaders(); + return this.getClients(); } /** diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx index f40934e785..d36e71afa2 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx @@ -439,10 +439,7 @@ function _mapDisplaySets( }; if (componentType === 'thumbnailNoImage') { - if ( - (dataSource.reject && dataSource.reject.series) || - (dataSource?.clientCanReject && dataSource.clientCanReject(ds.instances[0].clientName)) - ) { + if (dataSource.clientCanReject && dataSource.clientCanReject(ds.instances[0].clientName)) { thumbnailProps.canReject = !ds?.unsupported; thumbnailProps.onReject = () => { uiDialogService.create({ @@ -482,12 +479,11 @@ function _mapDisplaySets( switch (action.id) { case 'yes': try { - if (dataSource.reject) { - await dataSource.reject.series(ds.StudyInstanceUID, ds.SeriesInstanceUID); - } else { - const reject = dataSource.getClientReject(ds.instances[0].clientName); - await reject.series(ds.StudyInstanceUID, ds.SeriesInstanceUID); - } + await dataSource.reject.series( + ds.StudyInstanceUID, + ds.SeriesInstanceUID, + ds.instances[0].clientName + ); displaySetService.deleteDisplaySet(displaySetInstanceUID); uiDialogService.dismiss({ id: 'ds-reject-sr' }); uiNotificationService.show({