feat(hp): enable OHIF to run with partial metadata for large studies at the cost of less effective hanging protocol (#3804)
Co-authored-by: rodrigobasilio2022 <rodrigo.basilio@radicalimaging.com> Co-authored-by: rodrigobasilio2022 <114958722+rodrigobasilio2022@users.noreply.github.com>
This commit is contained in:
9 files changed
+240
-74
No files matched your search
@@ -28,17 +28,35 @@ 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 {string} name - Data source name
|
||||
* @param {string} wadoUriRoot - Legacy? (potentially unused/replaced)
|
||||
* @param {string} qidoRoot - Base URL to use for QIDO requests
|
||||
* @param {string} wadoRoot - Base URL to use for WADO requests
|
||||
* @param {boolean} qidoSupportsIncludeField - Whether QIDO supports the "Include" option to request additional fields in response
|
||||
* @param {string} imageRengering - wadors | ? (unsure of where/how this is used)
|
||||
* @param {string} thumbnailRendering - wadors | ? (unsure of where/how this is used)
|
||||
* @param {bool} supportsReject - Whether the server supports reject calls (i.e. DCM4CHEE)
|
||||
* @param {bool} lazyLoadStudy - "enableStudyLazyLoad"; Request series meta async instead of blocking
|
||||
* @param {string|bool} singlepart - indicates of the retrieves can fetch singlepart. Options are bulkdata, video, image or boolean true
|
||||
* @param {object} dicomWebConfig - Configuration for the DICOM Web API
|
||||
* @param {string} dicomWebConfig.name - Data source name
|
||||
* @param {string} dicomWebConfig.wadoUriRoot - Legacy? (potentially unused/replaced)
|
||||
* @param {string} dicomWebConfig.qidoRoot - Base URL to use for QIDO requests
|
||||
* @param {string} dicomWebConfig.wadoRoot - Base URL to use for WADO requests
|
||||
* @param {string} dicomWebConfig.wadoUri - Base URL to use for WADO URI requests
|
||||
* @param {boolean} dicomWebConfig.qidoSupportsIncludeField - Whether QIDO supports the "Include" option to request additional fields in response
|
||||
* @param {string} dicomWebConfig.imageRendering - wadors | ? (unsure of where/how this is used)
|
||||
* @param {string} dicomWebConfig.thumbnailRendering - wadors | ? (unsure of where/how this is used)
|
||||
* @param {boolean} dicomWebConfig.supportsReject - Whether the server supports reject calls (i.e. DCM4CHEE)
|
||||
* @param {boolean} dicomWebConfig.lazyLoadStudy - "enableStudyLazyLoad"; Request series meta async instead of blocking
|
||||
* @param {string|boolean} dicomWebConfig.singlepart - indicates if the retrieves can fetch singlepart. Options are bulkdata, video, image, or boolean true
|
||||
* @param {string} dicomWebConfig.requestTransferSyntaxUID - Transfer syntax to request from the server
|
||||
* @param {object} dicomWebConfig.acceptHeader - Accept header to use for requests
|
||||
* @param {boolean} dicomWebConfig.omitQuotationForMultipartRequest - Whether to omit quotation marks for multipart requests
|
||||
* @param {boolean} dicomWebConfig.supportsFuzzyMatching - Whether the server supports fuzzy matching
|
||||
* @param {boolean} dicomWebConfig.supportsWildcard - Whether the server supports wildcard matching
|
||||
* @param {boolean} dicomWebConfig.supportsNativeDICOMModel - Whether the server supports the native DICOM model
|
||||
* @param {boolean} dicomWebConfig.enableStudyLazyLoad - Whether to enable study lazy loading
|
||||
* @param {boolean} dicomWebConfig.enableRequestTag - Whether to enable request tag
|
||||
* @param {boolean} dicomWebConfig.enableStudyLazyLoad - Whether to enable study lazy loading
|
||||
* @param {boolean} dicomWebConfig.bulkDataURI - Whether to enable bulkDataURI
|
||||
* @param {function} dicomWebConfig.onConfiguration - Function that is called after the configuration is initialized
|
||||
* @param {boolean} dicomWebConfig.staticWado - Whether to use the static WADO client
|
||||
* @param {object} userAuthenticationService - User authentication service
|
||||
* @param {object} userAuthenticationService.getAuthorizationHeader - Function that returns the authorization header
|
||||
* @returns {object} - DICOM Web API object
|
||||
*/
|
||||
function createDicomWebApi(dicomWebConfig, servicesManager) {
|
||||
const { userAuthenticationService, customizationService } = servicesManager.services;
|
||||
@@ -191,6 +209,7 @@ function createDicomWebApi(dicomWebConfig, servicesManager) {
|
||||
sortCriteria,
|
||||
sortFunction,
|
||||
madeInClient = false,
|
||||
returnPromises = false,
|
||||
} = {}) => {
|
||||
if (!StudyInstanceUID) {
|
||||
throw new Error('Unable to query for SeriesMetadata without StudyInstanceUID');
|
||||
@@ -202,7 +221,8 @@ function createDicomWebApi(dicomWebConfig, servicesManager) {
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction,
|
||||
madeInClient
|
||||
madeInClient,
|
||||
returnPromises
|
||||
);
|
||||
}
|
||||
|
||||
@@ -336,7 +356,8 @@ function createDicomWebApi(dicomWebConfig, servicesManager) {
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction,
|
||||
madeInClient = false
|
||||
madeInClient = false,
|
||||
returnPromises = false
|
||||
) => {
|
||||
const enableStudyLazyLoad = true;
|
||||
wadoDicomWebClient.headers = generateWadoHeader();
|
||||
@@ -416,7 +437,7 @@ function createDicomWebApi(dicomWebConfig, servicesManager) {
|
||||
const naturalizedInstances = instances.map(addRetrieveBulkData);
|
||||
|
||||
// Adding instanceMetadata to OHIF MetadataProvider
|
||||
naturalizedInstances.forEach((instance, index) => {
|
||||
naturalizedInstances.forEach(instance => {
|
||||
instance.wadoRoot = dicomWebConfig.wadoRoot;
|
||||
instance.wadoUri = dicomWebConfig.wadoUri;
|
||||
|
||||
@@ -443,7 +464,7 @@ function createDicomWebApi(dicomWebConfig, servicesManager) {
|
||||
}
|
||||
|
||||
function setSuccessFlag() {
|
||||
const study = DicomMetadataStore.getStudy(StudyInstanceUID, madeInClient);
|
||||
const study = DicomMetadataStore.getStudy(StudyInstanceUID);
|
||||
if (!study) {
|
||||
return;
|
||||
}
|
||||
@@ -458,13 +479,22 @@ function createDicomWebApi(dicomWebConfig, servicesManager) {
|
||||
|
||||
DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata, madeInClient);
|
||||
|
||||
const seriesDeliveredPromises = seriesPromises.map(promise =>
|
||||
promise.then(instances => {
|
||||
const seriesDeliveredPromises = seriesPromises.map(promise => {
|
||||
if (!returnPromises) {
|
||||
promise?.start();
|
||||
}
|
||||
return promise.then(instances => {
|
||||
storeInstances(instances);
|
||||
})
|
||||
);
|
||||
await Promise.all(seriesDeliveredPromises);
|
||||
setSuccessFlag();
|
||||
});
|
||||
});
|
||||
|
||||
if (returnPromises) {
|
||||
Promise.all(seriesDeliveredPromises).then(() => setSuccessFlag());
|
||||
return seriesPromises;
|
||||
} else {
|
||||
await Promise.all(seriesDeliveredPromises);
|
||||
setSuccessFlag();
|
||||
}
|
||||
|
||||
return seriesSummaryMetadata;
|
||||
},
|
||||
@@ -496,7 +526,7 @@ function createDicomWebApi(dicomWebConfig, servicesManager) {
|
||||
|
||||
return imageIds;
|
||||
},
|
||||
getImageIdsForInstance({ instance, frame }) {
|
||||
getImageIdsForInstance({ instance, frame = undefined }) {
|
||||
const imageIds = getImageId({
|
||||
instance,
|
||||
frame,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { api } from 'dicomweb-client';
|
||||
* performing searches doesn't work. This version fixes the query issue
|
||||
* by manually implementing a query option.
|
||||
*/
|
||||
|
||||
export default class StaticWadoClient extends api.DICOMwebClient {
|
||||
static studyFilterKeys = {
|
||||
studyinstanceuid: '0020000D',
|
||||
|
||||
@@ -11,10 +11,17 @@ export default class RetrieveMetadataLoader {
|
||||
* @param {Object} client The dicomweb-client.
|
||||
* @param {Array} studyInstanceUID Study instance ui to be retrieved
|
||||
* @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 {Function} [sortSeries] - Custom sort function for series
|
||||
* @param {string} [filters.seriesInstanceUID] - series instance uid to filter results against
|
||||
* @param {Object} [sortCriteria] - Custom sort criteria used for series
|
||||
* @param {Function} [sortFunction] - Custom sort function for series
|
||||
*/
|
||||
constructor(client, studyInstanceUID, filters = {}, sortCriteria, sortFunction) {
|
||||
constructor(
|
||||
client,
|
||||
studyInstanceUID,
|
||||
filters = {},
|
||||
sortCriteria = undefined,
|
||||
sortFunction = undefined
|
||||
) {
|
||||
this.client = client;
|
||||
this.studyInstanceUID = studyInstanceUID;
|
||||
this.filters = filters;
|
||||
@@ -26,7 +33,6 @@ export default class RetrieveMetadataLoader {
|
||||
const preLoadData = await this.preLoad();
|
||||
const loadData = await this.load(preLoadData);
|
||||
const postLoadData = await this.posLoad(loadData);
|
||||
|
||||
return postLoadData;
|
||||
}
|
||||
|
||||
@@ -37,13 +43,9 @@ export default class RetrieveMetadataLoader {
|
||||
async runLoaders(loaders) {
|
||||
let result;
|
||||
for (const loader of loaders) {
|
||||
try {
|
||||
result = await loader();
|
||||
if (result && result.length) {
|
||||
break; // closes iterator in case data is retrieved successfully
|
||||
}
|
||||
} catch (e) {
|
||||
throw e;
|
||||
result = await loader();
|
||||
if (result && result.length) {
|
||||
break; // closes iterator in case data is retrieved successfully
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,58 @@
|
||||
import dcmjs from 'dcmjs';
|
||||
import { sortStudySeries, sortingCriteria } from '@ohif/core/src/utils/sortStudy';
|
||||
import { sortStudySeries } from '@ohif/core/src/utils/sortStudy';
|
||||
import RetrieveMetadataLoader from './retrieveMetadataLoader';
|
||||
|
||||
// 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.
|
||||
*
|
||||
@@ -16,12 +67,17 @@ function makeSeriesAsyncLoader(client, studyInstanceUID, seriesInstanceUIDList)
|
||||
hasNext() {
|
||||
return seriesInstanceUIDList.length > 0;
|
||||
},
|
||||
async next() {
|
||||
const seriesInstanceUID = seriesInstanceUIDList.shift();
|
||||
return client.retrieveSeriesMetadata({
|
||||
studyInstanceUID,
|
||||
seriesInstanceUID,
|
||||
next() {
|
||||
const { seriesInstanceUID, metadata } = seriesInstanceUIDList.shift();
|
||||
const promise = new DeferredPromise();
|
||||
promise.setMetadata(metadata);
|
||||
promise.setProcessFunction(() => {
|
||||
return client.retrieveSeriesMetadata({
|
||||
studyInstanceUID,
|
||||
seriesInstanceUID,
|
||||
});
|
||||
});
|
||||
return promise;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -40,15 +96,22 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader
|
||||
const preLoaders = [];
|
||||
const { studyInstanceUID, filters: { seriesInstanceUID } = {}, client } = this;
|
||||
|
||||
// asking to include Series Date, Series Time, Series Description
|
||||
// and Series Number in the series metadata returned to better sort series
|
||||
// in preLoad function
|
||||
let options = {
|
||||
studyInstanceUID,
|
||||
queryParams: {
|
||||
includefield: includeField,
|
||||
},
|
||||
};
|
||||
|
||||
if (seriesInstanceUID) {
|
||||
const options = {
|
||||
studyInstanceUID,
|
||||
queryParams: { SeriesInstanceUID: seriesInstanceUID },
|
||||
};
|
||||
options.queryParams.SeriesInstanceUID = seriesInstanceUID;
|
||||
preLoaders.push(client.searchForSeries.bind(client, options));
|
||||
}
|
||||
// Fallback preloader
|
||||
preLoaders.push(client.searchForSeries.bind(client, { studyInstanceUID }));
|
||||
preLoaders.push(client.searchForSeries.bind(client, options));
|
||||
|
||||
yield* preLoaders;
|
||||
}
|
||||
@@ -62,24 +125,23 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader
|
||||
const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary;
|
||||
const naturalized = result.map(naturalizeDataset);
|
||||
|
||||
return sortStudySeries(
|
||||
naturalized,
|
||||
sortCriteria || sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria,
|
||||
sortFunction
|
||||
);
|
||||
return sortStudySeries(naturalized, sortCriteria, sortFunction);
|
||||
}
|
||||
|
||||
async load(preLoadData) {
|
||||
const { client, studyInstanceUID } = this;
|
||||
|
||||
const seriesInstanceUIDs = preLoadData.map(s => s.SeriesInstanceUID);
|
||||
const seriesInstanceUIDs = preLoadData.map(seriesMetadata => {
|
||||
return { seriesInstanceUID: seriesMetadata.SeriesInstanceUID, metadata: seriesMetadata };
|
||||
});
|
||||
|
||||
const seriesAsyncLoader = makeSeriesAsyncLoader(client, studyInstanceUID, seriesInstanceUIDs);
|
||||
|
||||
const promises = [];
|
||||
|
||||
while (seriesAsyncLoader.hasNext()) {
|
||||
promises.push(seriesAsyncLoader.next());
|
||||
const promise = seriesAsyncLoader.next();
|
||||
promises.push(promise);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -14,6 +14,7 @@ const defaultProtocol = {
|
||||
editableBy: {},
|
||||
protocolMatchingRules: [],
|
||||
toolGroupIds: ['default'],
|
||||
hpInitiationCriteria: { minSeriesLoaded: 1 },
|
||||
// -1 would be used to indicate active only, whereas other values are
|
||||
// the number of required priors referenced - so 0 means active with
|
||||
// 0 or more priors.
|
||||
|
||||
Reference in new issue
Block a user