feat: Merge Data Source (#3788)
Add the ability to merge two different series queries to generate a complete study query result. Provides basic support for other types of merges, but those aren't yet added as full features.
This commit is contained in:
1 parent
1be5613946
commit
c4ff2c2f09
13 files changed
+604
-20
No files matched your search
@@ -0,0 +1,252 @@
|
||||
import { DicomMetadataStore, IWebApiDataSource } from '@ohif/core';
|
||||
import { get, uniqBy } from 'lodash';
|
||||
import {
|
||||
MergeConfig,
|
||||
CallForAllDataSourcesAsyncOptions,
|
||||
CallForAllDataSourcesOptions,
|
||||
CallForDefaultDataSourceOptions,
|
||||
CallByRetrieveAETitleOptions,
|
||||
MergeMap,
|
||||
} from './types';
|
||||
|
||||
export const mergeMap: MergeMap = {
|
||||
'query.studies.search': {
|
||||
mergeKey: 'studyInstanceUid',
|
||||
tagFunc: x => x,
|
||||
},
|
||||
'query.series.search': {
|
||||
mergeKey: 'seriesInstanceUid',
|
||||
tagFunc: (series, sourceName) => {
|
||||
series.forEach(series => {
|
||||
series.RetrieveAETitle = sourceName;
|
||||
DicomMetadataStore.updateSeriesMetadata(series);
|
||||
});
|
||||
return series;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Calls all data sources asynchronously and merges the results.
|
||||
* @param {CallForAllDataSourcesAsyncOptions} options - The options for calling all data sources.
|
||||
* @param {string} options.path - The path to the function to be called on each data source.
|
||||
* @param {unknown[]} options.args - The arguments to be passed to the function.
|
||||
* @param {ExtensionManager} options.extensionManager - The extension manager.
|
||||
* @param {string[]} options.dataSourceNames - The names of the data sources to be called.
|
||||
* @returns {Promise<unknown[]>} - A promise that resolves to the merged data from all data sources.
|
||||
*/
|
||||
export const callForAllDataSourcesAsync = async ({
|
||||
mergeMap,
|
||||
path,
|
||||
args,
|
||||
extensionManager,
|
||||
dataSourceNames,
|
||||
}: CallForAllDataSourcesAsyncOptions) => {
|
||||
const { mergeKey, tagFunc } = mergeMap[path] || { tagFunc: x => x };
|
||||
|
||||
const dataSourceDefs = Object.values(extensionManager.dataSourceDefs);
|
||||
const promises = [];
|
||||
const mergedData = [];
|
||||
|
||||
for (const dataSourceDef of dataSourceDefs) {
|
||||
const { configuration, sourceName } = dataSourceDef;
|
||||
if (!!configuration && dataSourceNames.includes(sourceName)) {
|
||||
const [dataSource] = extensionManager.getDataSources(sourceName);
|
||||
const func = get(dataSource, path);
|
||||
const promise = func.apply(dataSource, args);
|
||||
promises.push(promise.then(data => mergedData.push(tagFunc(data, sourceName))));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
|
||||
return uniqBy(mergedData.flat(), obj => obj[mergeKey]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calls all data sources that match the provided names and merges their data.
|
||||
* @param options - The options for calling all data sources.
|
||||
* @param options.path - The path to the function to be called on each data source.
|
||||
* @param options.args - The arguments to be passed to the function.
|
||||
* @param options.extensionManager - The extension manager instance.
|
||||
* @param options.dataSourceNames - The names of the data sources to be called.
|
||||
* @returns The merged data from all the matching data sources.
|
||||
*/
|
||||
export const callForAllDataSources = ({
|
||||
path,
|
||||
args,
|
||||
extensionManager,
|
||||
dataSourceNames,
|
||||
}: CallForAllDataSourcesOptions) => {
|
||||
const dataSourceDefs = Object.values(extensionManager.dataSourceDefs);
|
||||
const mergedData = [];
|
||||
for (const dataSourceDef of dataSourceDefs) {
|
||||
const { configuration, sourceName } = dataSourceDef;
|
||||
if (!!configuration && dataSourceNames.includes(sourceName)) {
|
||||
const [dataSource] = extensionManager.getDataSources(sourceName);
|
||||
const func = get(dataSource, path);
|
||||
const data = func.apply(dataSource, args);
|
||||
mergedData.push(data);
|
||||
}
|
||||
}
|
||||
return mergedData.flat();
|
||||
};
|
||||
|
||||
/**
|
||||
* Calls the default data source function specified by the given path with the provided arguments.
|
||||
* @param {CallForDefaultDataSourceOptions} options - The options for calling the default data source.
|
||||
* @param {string} options.path - The path to the function within the default data source.
|
||||
* @param {unknown[]} options.args - The arguments to pass to the function.
|
||||
* @param {string} options.defaultDataSourceName - The name of the default data source.
|
||||
* @param {ExtensionManager} options.extensionManager - The extension manager instance.
|
||||
* @returns {unknown} - The result of calling the default data source function.
|
||||
*/
|
||||
export const callForDefaultDataSource = ({
|
||||
path,
|
||||
args,
|
||||
defaultDataSourceName,
|
||||
extensionManager,
|
||||
}: CallForDefaultDataSourceOptions) => {
|
||||
const [dataSource] = extensionManager.getDataSources(defaultDataSourceName);
|
||||
const func = get(dataSource, path);
|
||||
return func.apply(dataSource, args);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calls the data source specified by the RetrieveAETitle of the given display set.
|
||||
* @typedef {Object} CallByRetrieveAETitleOptions
|
||||
* @property {string} path - The path of the method to call on the data source.
|
||||
* @property {unknown[]} args - The arguments to pass to the method.
|
||||
* @property {string} defaultDataSourceName - The name of the default data source.
|
||||
* @property {ExtensionManager} extensionManager - The extension manager.
|
||||
*/
|
||||
export const callByRetrieveAETitle = ({
|
||||
path,
|
||||
args,
|
||||
defaultDataSourceName,
|
||||
extensionManager,
|
||||
}: CallByRetrieveAETitleOptions) => {
|
||||
const [displaySet] = args;
|
||||
const seriesMetadata = DicomMetadataStore.getSeries(
|
||||
displaySet.StudyInstanceUID,
|
||||
displaySet.SeriesInstanceUID
|
||||
);
|
||||
const [dataSource] = extensionManager.getDataSources(
|
||||
seriesMetadata.RetrieveAETitle || defaultDataSourceName
|
||||
);
|
||||
return dataSource[path](...args);
|
||||
};
|
||||
|
||||
function createMergeDataSourceApi(
|
||||
mergeConfig: MergeConfig,
|
||||
UserAuthenticationService: unknown,
|
||||
extensionManager
|
||||
) {
|
||||
const { seriesMerge } = mergeConfig;
|
||||
const { dataSourceNames, defaultDataSourceName } = seriesMerge;
|
||||
|
||||
const implementation = {
|
||||
initialize: (...args: unknown[]) =>
|
||||
callForAllDataSources({ path: 'initialize', args, extensionManager, dataSourceNames }),
|
||||
query: {
|
||||
studies: {
|
||||
search: (...args: unknown[]) =>
|
||||
callForAllDataSourcesAsync({
|
||||
mergeMap,
|
||||
path: 'query.studies.search',
|
||||
args,
|
||||
extensionManager,
|
||||
dataSourceNames,
|
||||
}),
|
||||
},
|
||||
series: {
|
||||
search: (...args: unknown[]) =>
|
||||
callForAllDataSourcesAsync({
|
||||
mergeMap,
|
||||
path: 'query.series.search',
|
||||
args,
|
||||
extensionManager,
|
||||
dataSourceNames,
|
||||
}),
|
||||
},
|
||||
instances: {
|
||||
search: (...args: unknown[]) =>
|
||||
callForAllDataSourcesAsync({
|
||||
mergeMap,
|
||||
path: 'query.instances.search',
|
||||
args,
|
||||
extensionManager,
|
||||
dataSourceNames,
|
||||
}),
|
||||
},
|
||||
},
|
||||
retrieve: {
|
||||
bulkDataURI: (...args: unknown[]) =>
|
||||
callForAllDataSourcesAsync({
|
||||
mergeMap,
|
||||
path: 'retrieve.bulkDataURI',
|
||||
args,
|
||||
extensionManager,
|
||||
dataSourceNames,
|
||||
}),
|
||||
directURL: (...args: unknown[]) =>
|
||||
callForDefaultDataSource({
|
||||
path: 'retrieve.directURL',
|
||||
args,
|
||||
defaultDataSourceName,
|
||||
extensionManager,
|
||||
}),
|
||||
series: {
|
||||
metadata: (...args: unknown[]) =>
|
||||
callForAllDataSourcesAsync({
|
||||
mergeMap,
|
||||
path: 'retrieve.series.metadata',
|
||||
args,
|
||||
extensionManager,
|
||||
dataSourceNames,
|
||||
}),
|
||||
},
|
||||
},
|
||||
store: {
|
||||
dicom: (...args: unknown[]) =>
|
||||
callForDefaultDataSource({
|
||||
path: 'store.dicom',
|
||||
args,
|
||||
defaultDataSourceName,
|
||||
extensionManager,
|
||||
}),
|
||||
},
|
||||
deleteStudyMetadataPromise: (...args: unknown[]) =>
|
||||
callForAllDataSources({
|
||||
path: 'deleteStudyMetadataPromise',
|
||||
args,
|
||||
extensionManager,
|
||||
dataSourceNames,
|
||||
}),
|
||||
getImageIdsForDisplaySet: (...args: unknown[]) =>
|
||||
callByRetrieveAETitle({
|
||||
path: 'getImageIdsForDisplaySet',
|
||||
args,
|
||||
defaultDataSourceName,
|
||||
extensionManager,
|
||||
}),
|
||||
getImageIdsForInstance: (...args: unknown[]) =>
|
||||
callByRetrieveAETitle({
|
||||
path: 'getImageIdsForDisplaySet',
|
||||
args,
|
||||
defaultDataSourceName,
|
||||
extensionManager,
|
||||
}),
|
||||
getStudyInstanceUIDs: (...args: unknown[]) =>
|
||||
callForAllDataSources({
|
||||
path: 'getStudyInstanceUIDs',
|
||||
args,
|
||||
extensionManager,
|
||||
dataSourceNames,
|
||||
}),
|
||||
};
|
||||
|
||||
return IWebApiDataSource.create(implementation);
|
||||
}
|
||||
|
||||
export { createMergeDataSourceApi };
|
||||
Reference in new issue
Block a user