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:
parent
1be5613946
commit
c4ff2c2f09
@ -45,6 +45,7 @@ module.exports = {
|
||||
'@babel/plugin-proposal-object-rest-spread',
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
'@babel/plugin-transform-regenerator',
|
||||
'@babel/transform-destructuring',
|
||||
'@babel/plugin-transform-runtime',
|
||||
'@babel/plugin-transform-typescript',
|
||||
],
|
||||
|
||||
1
extensions/default/babel.config.js
Normal file
1
extensions/default/babel.config.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('../../babel.config.js');
|
||||
17
extensions/default/jest.config.js
Normal file
17
extensions/default/jest.config.js
Normal file
@ -0,0 +1,17 @@
|
||||
const base = require('../../jest.config.base.js');
|
||||
const pkg = require('./package');
|
||||
|
||||
module.exports = {
|
||||
...base,
|
||||
name: pkg.name,
|
||||
displayName: pkg.name,
|
||||
moduleNameMapper: {
|
||||
...base.moduleNameMapper,
|
||||
'@ohif/(.*)': '<rootDir>/../../platform/$1/src',
|
||||
},
|
||||
// rootDir: "../.."
|
||||
// testMatch: [
|
||||
// //`<rootDir>/platform/${pack.name}/**/*.spec.js`
|
||||
// "<rootDir>/platform/app/**/*.test.js"
|
||||
// ]
|
||||
};
|
||||
@ -1,14 +1,14 @@
|
||||
import ContextMenuItemsBuilder from './ContextMenuItemsBuilder';
|
||||
import * as ContextMenuItemsBuilder from './ContextMenuItemsBuilder';
|
||||
|
||||
const menus = [
|
||||
{
|
||||
id: 'one',
|
||||
selector: ({ value }) => value === 'one',
|
||||
selector: ({ value } = {}) => value === 'one',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
id: 'two',
|
||||
selector: ({ value }) => value === 'two',
|
||||
selector: ({ value } = {}) => value === 'two',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
@ -17,13 +17,13 @@ const menus = [
|
||||
},
|
||||
];
|
||||
|
||||
const menuBuilder = new ContextMenuItemsBuilder();
|
||||
|
||||
describe('ContextMenuItemsBuilder', () => {
|
||||
test('findMenuDefault', () => {
|
||||
expect(menuBuilder.findMenuDefault(menus, {})).toBe(menus[2]);
|
||||
expect(menuBuilder.findMenuDefault(menus, { value: 'two' })).toBe(menus[1]);
|
||||
expect(menuBuilder.findMenuDefault([], {})).toBeUndefined();
|
||||
expect(menuBuilder.findMenuDefault(undefined, undefined)).toBeNull();
|
||||
expect(ContextMenuItemsBuilder.findMenuDefault(menus, {})).toBe(menus[2]);
|
||||
expect(
|
||||
ContextMenuItemsBuilder.findMenuDefault(menus, { selectorProps: { value: 'two' } })
|
||||
).toBe(menus[1]);
|
||||
expect(ContextMenuItemsBuilder.findMenuDefault([], {})).toBeUndefined();
|
||||
expect(ContextMenuItemsBuilder.findMenuDefault(undefined, undefined)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@ -140,7 +140,7 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
||||
instances: {
|
||||
search: (studyInstanceUid, queryParameters) => {
|
||||
qidoDicomWebClient.headers = getAuthrorizationHeader();
|
||||
qidoSearch.call(undefined, qidoDicomWebClient, studyInstanceUid, null, queryParameters);
|
||||
return qidoSearch.call(undefined, qidoDicomWebClient, studyInstanceUid, null, queryParameters);
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -262,7 +262,8 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
||||
enableStudyLazyLoad,
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction
|
||||
sortFunction,
|
||||
dicomWebConfig
|
||||
);
|
||||
|
||||
// first naturalize the data
|
||||
@ -314,6 +315,8 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
||||
Object.keys(instancesPerSeries).forEach(seriesInstanceUID =>
|
||||
DicomMetadataStore.addInstances(instancesPerSeries[seriesInstanceUID], madeInClient)
|
||||
);
|
||||
|
||||
return seriesSummaryMetadata;
|
||||
},
|
||||
|
||||
_retrieveSeriesMetadataAsync: async (
|
||||
@ -333,7 +336,8 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
||||
enableStudyLazyLoad,
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction
|
||||
sortFunction,
|
||||
dicomWebConfig
|
||||
);
|
||||
|
||||
/**
|
||||
@ -444,6 +448,8 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
||||
);
|
||||
await Promise.all(seriesDeliveredPromises);
|
||||
setSuccessFlag();
|
||||
|
||||
return seriesSummaryMetadata;
|
||||
},
|
||||
deleteStudyMetadataPromise,
|
||||
getImageIdsForDisplaySet(displaySet) {
|
||||
|
||||
@ -25,7 +25,8 @@ export function retrieveStudyMetadata(
|
||||
enableStudyLazyLoad,
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction
|
||||
sortFunction,
|
||||
dicomWebConfig = {}
|
||||
) {
|
||||
// @TODO: Whenever a study metadata request has failed, its related promise will be rejected once and for all
|
||||
// and further requests for that metadata will always fail. On failure, we probably need to remove the
|
||||
@ -38,9 +39,11 @@ export function retrieveStudyMetadata(
|
||||
throw new Error(`${moduleName}: Required 'StudyInstanceUID' parameter not provided.`);
|
||||
}
|
||||
|
||||
const promiseId = `${dicomWebConfig.name}:${StudyInstanceUID}`;
|
||||
|
||||
// Already waiting on result? Return cached promise
|
||||
if (StudyMetaDataPromises.has(StudyInstanceUID)) {
|
||||
return StudyMetaDataPromises.get(StudyInstanceUID);
|
||||
if (StudyMetaDataPromises.has(promiseId)) {
|
||||
return StudyMetaDataPromises.get(promiseId);
|
||||
}
|
||||
|
||||
let promise;
|
||||
@ -71,7 +74,7 @@ export function retrieveStudyMetadata(
|
||||
}
|
||||
|
||||
// Store the promise in cache
|
||||
StudyMetaDataPromises.set(StudyInstanceUID, promise);
|
||||
StudyMetaDataPromises.set(promiseId, promise);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
203
extensions/default/src/MergeDataSource/index.test.js
Normal file
203
extensions/default/src/MergeDataSource/index.test.js
Normal file
@ -0,0 +1,203 @@
|
||||
import { DicomMetadataStore, IWebApiDataSource } from '@ohif/core';
|
||||
import {
|
||||
mergeMap,
|
||||
callForAllDataSourcesAsync,
|
||||
callForAllDataSources,
|
||||
callForDefaultDataSource,
|
||||
callByRetrieveAETitle,
|
||||
createMergeDataSourceApi,
|
||||
} from './index';
|
||||
|
||||
jest.mock('@ohif/core');
|
||||
|
||||
describe('MergeDataSource', () => {
|
||||
let path,
|
||||
sourceName,
|
||||
mergeConfig,
|
||||
extensionManager,
|
||||
series1,
|
||||
series2,
|
||||
series3,
|
||||
series4,
|
||||
mergeKey,
|
||||
tagFunc,
|
||||
dataSourceAndSeriesMap,
|
||||
dataSourceAndUIDsMap,
|
||||
dataSourceAndDSMap,
|
||||
pathSync;
|
||||
|
||||
beforeAll(() => {
|
||||
path = 'query.series.search';
|
||||
pathSync = 'getImageIdsForInstance';
|
||||
tagFunc = jest.fn((data, sourceName) =>
|
||||
data.map(item => ({ ...item, RetrieveAETitle: sourceName }))
|
||||
);
|
||||
sourceName = 'dicomweb1';
|
||||
mergeKey = 'seriesInstanceUid';
|
||||
series1 = { [mergeKey]: '123' };
|
||||
series2 = { [mergeKey]: '234' };
|
||||
series3 = { [mergeKey]: '345' };
|
||||
series4 = { [mergeKey]: '456' };
|
||||
mergeConfig = {
|
||||
seriesMerge: {
|
||||
dataSourceNames: ['dicomweb1', 'dicomweb2'],
|
||||
defaultDataSourceName: 'dicomweb1',
|
||||
},
|
||||
};
|
||||
dataSourceAndSeriesMap = {
|
||||
dataSource1: series1,
|
||||
dataSource2: series2,
|
||||
dataSource3: series3,
|
||||
};
|
||||
dataSourceAndUIDsMap = {
|
||||
dataSource1: ['123'],
|
||||
dataSource2: ['234'],
|
||||
dataSource3: ['345'],
|
||||
};
|
||||
dataSourceAndDSMap = {
|
||||
dataSource1: {
|
||||
displaySet: {
|
||||
StudyInstanceUID: '123',
|
||||
SeriesInstanceUID: '123',
|
||||
},
|
||||
},
|
||||
dataSource2: {
|
||||
displaySet: {
|
||||
StudyInstanceUID: '234',
|
||||
SeriesInstanceUID: '234',
|
||||
},
|
||||
},
|
||||
dataSource3: {
|
||||
displaySet: {
|
||||
StudyInstanceUID: '345',
|
||||
SeriesInstanceUID: '345',
|
||||
},
|
||||
},
|
||||
};
|
||||
extensionManager = {
|
||||
dataSourceDefs: {
|
||||
dataSource1: {
|
||||
sourceName: 'dataSource1',
|
||||
configuration: {},
|
||||
},
|
||||
dataSource2: {
|
||||
sourceName: 'dataSource2',
|
||||
configuration: {},
|
||||
},
|
||||
dataSource3: {
|
||||
sourceName: 'dataSource3',
|
||||
configuration: {},
|
||||
},
|
||||
},
|
||||
getDataSources: jest.fn(dataSourceName => [
|
||||
{
|
||||
[path]: jest.fn().mockResolvedValue([dataSourceAndSeriesMap[dataSourceName]]),
|
||||
},
|
||||
]),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('callForAllDataSourcesAsync', () => {
|
||||
it('should call the correct functions and return the merged data', async () => {
|
||||
/** Arrange */
|
||||
extensionManager.getDataSources = jest.fn(dataSourceName => [
|
||||
{
|
||||
[path]: jest.fn().mockResolvedValue([dataSourceAndSeriesMap[dataSourceName]]),
|
||||
},
|
||||
]);
|
||||
|
||||
/** Act */
|
||||
const data = await callForAllDataSourcesAsync({
|
||||
mergeMap,
|
||||
path,
|
||||
args: [],
|
||||
extensionManager,
|
||||
dataSourceNames: ['dataSource1', 'dataSource2'],
|
||||
});
|
||||
|
||||
/** Assert */
|
||||
expect(extensionManager.getDataSources).toHaveBeenCalledTimes(2);
|
||||
expect(extensionManager.getDataSources).toHaveBeenCalledWith('dataSource1');
|
||||
expect(extensionManager.getDataSources).toHaveBeenCalledWith('dataSource2');
|
||||
expect(data).toEqual([series1, series2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('callForAllDataSources', () => {
|
||||
it('should call the correct functions and return the merged data', () => {
|
||||
/** Arrange */
|
||||
extensionManager.getDataSources = jest.fn(dataSourceName => [
|
||||
{
|
||||
[pathSync]: () => dataSourceAndUIDsMap[dataSourceName],
|
||||
},
|
||||
]);
|
||||
|
||||
/** Act */
|
||||
const data = callForAllDataSources({
|
||||
path: pathSync,
|
||||
args: [],
|
||||
extensionManager,
|
||||
dataSourceNames: ['dataSource2', 'dataSource3'],
|
||||
});
|
||||
|
||||
/** Assert */
|
||||
expect(extensionManager.getDataSources).toHaveBeenCalledTimes(2);
|
||||
expect(extensionManager.getDataSources).toHaveBeenCalledWith('dataSource2');
|
||||
expect(extensionManager.getDataSources).toHaveBeenCalledWith('dataSource3');
|
||||
expect(data).toEqual(['234', '345']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('callForDefaultDataSource', () => {
|
||||
it('should call the correct function and return the data', () => {
|
||||
/** Arrange */
|
||||
extensionManager.getDataSources = jest.fn(dataSourceName => [
|
||||
{
|
||||
[pathSync]: () => dataSourceAndUIDsMap[dataSourceName],
|
||||
},
|
||||
]);
|
||||
|
||||
/** Act */
|
||||
const data = callForDefaultDataSource({
|
||||
path: pathSync,
|
||||
args: [],
|
||||
extensionManager,
|
||||
defaultDataSourceName: 'dataSource2',
|
||||
});
|
||||
|
||||
/** Assert */
|
||||
expect(extensionManager.getDataSources).toHaveBeenCalledTimes(1);
|
||||
expect(extensionManager.getDataSources).toHaveBeenCalledWith('dataSource2');
|
||||
expect(data).toEqual(['234']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('callByRetrieveAETitle', () => {
|
||||
it('should call the correct function and return the data', () => {
|
||||
/** Arrange */
|
||||
DicomMetadataStore.getSeries.mockImplementationOnce(() => [series2]);
|
||||
extensionManager.getDataSources = jest.fn(dataSourceName => [
|
||||
{
|
||||
[pathSync]: () => dataSourceAndUIDsMap[dataSourceName],
|
||||
},
|
||||
]);
|
||||
|
||||
/** Act */
|
||||
const data = callByRetrieveAETitle({
|
||||
path: pathSync,
|
||||
args: [dataSourceAndDSMap['dataSource2']],
|
||||
extensionManager,
|
||||
defaultDataSourceName: 'dataSource2',
|
||||
});
|
||||
|
||||
/** Assert */
|
||||
expect(extensionManager.getDataSources).toHaveBeenCalledTimes(1);
|
||||
expect(extensionManager.getDataSources).toHaveBeenCalledWith('dataSource2');
|
||||
expect(data).toEqual(['234']);
|
||||
});
|
||||
});
|
||||
});
|
||||
252
extensions/default/src/MergeDataSource/index.ts
Normal file
252
extensions/default/src/MergeDataSource/index.ts
Normal file
@ -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 };
|
||||
44
extensions/default/src/MergeDataSource/types.ts
Normal file
44
extensions/default/src/MergeDataSource/types.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { ExtensionManager } from '@ohif/core';
|
||||
|
||||
export type MergeMap = {
|
||||
[key: string]: {
|
||||
mergeKey: string;
|
||||
tagFunc: (data: unknown[], sourceName: string) => unknown[];
|
||||
};
|
||||
};
|
||||
|
||||
export type CallForAllDataSourcesAsyncOptions = {
|
||||
mergeMap: object;
|
||||
path: string;
|
||||
args: unknown[];
|
||||
dataSourceNames: string[];
|
||||
extensionManager: ExtensionManager;
|
||||
};
|
||||
|
||||
export type CallForAllDataSourcesOptions = {
|
||||
path: string;
|
||||
args: unknown[];
|
||||
dataSourceNames: string[];
|
||||
extensionManager: ExtensionManager;
|
||||
};
|
||||
|
||||
export type CallForDefaultDataSourceOptions = {
|
||||
path: string;
|
||||
args: unknown[];
|
||||
defaultDataSourceName: string;
|
||||
extensionManager: ExtensionManager;
|
||||
};
|
||||
|
||||
export type CallByRetrieveAETitleOptions = {
|
||||
path: string;
|
||||
args: unknown[];
|
||||
defaultDataSourceName: string;
|
||||
extensionManager: ExtensionManager;
|
||||
};
|
||||
|
||||
export type MergeConfig = {
|
||||
seriesMerge: {
|
||||
dataSourceNames: string[];
|
||||
defaultDataSourceName: string;
|
||||
};
|
||||
};
|
||||
@ -6,6 +6,7 @@ import { createDicomWebApi } from './DicomWebDataSource/index.js';
|
||||
import { createDicomJSONApi } from './DicomJSONDataSource/index.js';
|
||||
import { createDicomLocalApi } from './DicomLocalDataSource/index.js';
|
||||
import { createDicomWebProxyApi } from './DicomWebProxyDataSource/index.js';
|
||||
import { createMergeDataSourceApi } from './MergeDataSource/index';
|
||||
|
||||
/**
|
||||
*
|
||||
@ -32,6 +33,11 @@ function getDataSourcesModule() {
|
||||
type: 'localApi',
|
||||
createDataSource: createDicomLocalApi,
|
||||
},
|
||||
{
|
||||
name: 'merge',
|
||||
type: 'mergeApi',
|
||||
createDataSource: createMergeDataSourceApi,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -464,7 +464,8 @@ export default class ExtensionManager extends PubSubService {
|
||||
const { userAuthenticationService } = this._servicesManager.services;
|
||||
const dataSourceInstance = module.createDataSource(
|
||||
dataSourceDef.configuration,
|
||||
userAuthenticationService
|
||||
userAuthenticationService,
|
||||
this
|
||||
);
|
||||
|
||||
this.dataSourceMap[dataSourceDef.sourceName] = [dataSourceInstance];
|
||||
|
||||
@ -186,6 +186,18 @@ const BaseImplementation = {
|
||||
madeInClient,
|
||||
});
|
||||
},
|
||||
updateSeriesMetadata(seriesMetadata) {
|
||||
const { StudyInstanceUID, SeriesInstanceUID } = seriesMetadata;
|
||||
const series = _getSeries(StudyInstanceUID, SeriesInstanceUID);
|
||||
if (!series) {
|
||||
return;
|
||||
}
|
||||
|
||||
const study = _getStudy(StudyInstanceUID);
|
||||
if (study) {
|
||||
study.setSeriesMetadata(SeriesInstanceUID, seriesMetadata);
|
||||
}
|
||||
},
|
||||
addSeriesMetadata(seriesSummaryMetadata, madeInClient = false) {
|
||||
if (!seriesSummaryMetadata || !seriesSummaryMetadata.length || !seriesSummaryMetadata[0]) {
|
||||
return;
|
||||
|
||||
@ -81,7 +81,6 @@ You can take a look at `dicomweb` data source implementation to get an idea
|
||||
`extensions/default/src/DicomWebDataSource/index.js` but here here are some
|
||||
important api endpoints that you need to implement:
|
||||
|
||||
|
||||
- `initialize`: This method is called when the data source is first created in the mode.tsx, it is used to initialize the data source and set the configuration. For instance, `dicomwebDatasource` uses this method to grab the StudyInstanceUID from the URL and set it as the active study, as opposed to `dicomJSONDatasource` which uses url in the browser to fetch the data and store it in a cache
|
||||
- `query.studies.search`: This is used in the study panel on the left to fetch the prior studies for the same MRN which is then used to display on the `All` tab. it is also used in the Worklist to show all the studies from the server.
|
||||
- `query.series.search`: This is used to fetch the series information for a given study that is expanded in the Worklist.
|
||||
@ -89,8 +88,6 @@ important api endpoints that you need to implement:
|
||||
- `retrieve.series.metadata`: It is a crucial end point that is used to fetch series level metadata which for hanging displaySets and displaySet creation.
|
||||
- `store.dicom`: If you don't need store functionality, you can skip this method. This is used to store the data in the backend.
|
||||
|
||||
|
||||
|
||||
## Static WADO Client
|
||||
|
||||
If the configuration for the data source has the value staticWado set, then it
|
||||
@ -172,3 +169,44 @@ extensionManager.updateDataSourceConfiguration( "dicomweb",
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## Merge Data Source
|
||||
The built-in merge data source is a useful tool for combining results from multiple data sources.
|
||||
Currently, this data source only supports merging at the series level. This means that series from data source 'A'
|
||||
and series from data source 'B' will be retrieved under the same study. If the same series exists in both data sources,
|
||||
the first series arrived is the one that gets stored, and any other conflicting series will be ignored.
|
||||
|
||||
The merge data source is particularly useful when dealing with derived data that is generated and stored in different servers.
|
||||
For example, it can be used to retrieve annotation series from one data source and input data (images) from another data source.
|
||||
|
||||
A default data source can be defined as shown below. This allows defining which of the servers should be the
|
||||
fallback server in case something goes wrong.
|
||||
|
||||
Configuration Example:
|
||||
```js
|
||||
window.config = {
|
||||
...
|
||||
dataSources: [
|
||||
{
|
||||
sourceName: 'merge',
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.merge',
|
||||
configuration: {
|
||||
name: 'merge',
|
||||
friendlyName: 'Merge dicomweb-1 and dicomweb-2 data at the series level',
|
||||
seriesMerge: {
|
||||
dataSourceNames: ['dicomweb-1', 'dicomweb-2'],
|
||||
defaultDataSourceName: 'dicomweb-1'
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
sourceName: 'dicomweb-1',
|
||||
...
|
||||
},
|
||||
{
|
||||
sourceName: 'dicomweb-2',
|
||||
...
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
Loading…
Reference in New Issue
Block a user