init
This commit is contained in:
35
extensions/default/src/DicomWebDataSource/dcm4cheeReject.js
Normal file
35
extensions/default/src/DicomWebDataSource/dcm4cheeReject.js
Normal file
@@ -0,0 +1,35 @@
|
||||
export default function (wadoRoot) {
|
||||
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);
|
||||
|
||||
//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 204:
|
||||
resolve(xhr.responseText);
|
||||
|
||||
break;
|
||||
case 404:
|
||||
reject('Your dataSource does not support reject functionality');
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
280
extensions/default/src/DicomWebDataSource/exampleInstances.js
Normal file
280
extensions/default/src/DicomWebDataSource/exampleInstances.js
Normal file
@@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
585
extensions/default/src/DicomWebDataSource/index.js
Normal file
585
extensions/default/src/DicomWebDataSource/index.js
Normal file
@@ -0,0 +1,585 @@
|
||||
import { api } from 'dicomweb-client';
|
||||
import { DicomMetadataStore, IWebApiDataSource, utils, errorHandler, classes } from '@ohif/core';
|
||||
|
||||
import {
|
||||
mapParams,
|
||||
search as 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;
|
||||
|
||||
const { naturalizeDataset, denaturalizeDataset } = DicomMetaDictionary;
|
||||
|
||||
const ImplementationClassUID = '2.25.270695996825855179949881587723571202391.2.0.0';
|
||||
const ImplementationVersionName = 'OHIF-VIEWER-2.0.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 {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;
|
||||
let dicomWebConfigCopy,
|
||||
qidoConfig,
|
||||
wadoConfig,
|
||||
qidoDicomWebClient,
|
||||
wadoDicomWebClient,
|
||||
getAuthrorizationHeader,
|
||||
generateWadoHeader;
|
||||
|
||||
const implementation = {
|
||||
initialize: ({ params, query }) => {
|
||||
if (dicomWebConfig.onConfiguration && typeof dicomWebConfig.onConfiguration === 'function') {
|
||||
dicomWebConfig = dicomWebConfig.onConfiguration(dicomWebConfig, {
|
||||
params,
|
||||
query,
|
||||
});
|
||||
}
|
||||
|
||||
dicomWebConfigCopy = JSON.parse(JSON.stringify(dicomWebConfig));
|
||||
|
||||
getAuthrorizationHeader = () => {
|
||||
const xhrRequestHeaders = {};
|
||||
const authHeaders = userAuthenticationService.getAuthorizationHeader();
|
||||
if (authHeaders && authHeaders.Authorization) {
|
||||
xhrRequestHeaders.Authorization = authHeaders.Authorization;
|
||||
}
|
||||
return xhrRequestHeaders;
|
||||
};
|
||||
|
||||
generateWadoHeader = () => {
|
||||
const authorizationHeader = getAuthrorizationHeader();
|
||||
//Generate accept header depending on config params
|
||||
const formattedAcceptHeader = utils.generateAcceptHeader(
|
||||
dicomWebConfig.acceptHeader,
|
||||
dicomWebConfig.requestTransferSyntaxUID,
|
||||
dicomWebConfig.omitQuotationForMultipartRequest
|
||||
);
|
||||
|
||||
return {
|
||||
...authorizationHeader,
|
||||
Accept: formattedAcceptHeader,
|
||||
};
|
||||
};
|
||||
|
||||
qidoConfig = {
|
||||
url: dicomWebConfig.qidoRoot,
|
||||
staticWado: dicomWebConfig.staticWado,
|
||||
singlepart: dicomWebConfig.singlepart,
|
||||
headers: userAuthenticationService.getAuthorizationHeader(),
|
||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||
};
|
||||
|
||||
wadoConfig = {
|
||||
url: dicomWebConfig.wadoRoot,
|
||||
staticWado: dicomWebConfig.staticWado,
|
||||
singlepart: dicomWebConfig.singlepart,
|
||||
headers: userAuthenticationService.getAuthorizationHeader(),
|
||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||
};
|
||||
|
||||
// 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 = getAuthrorizationHeader();
|
||||
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 = getAuthrorizationHeader();
|
||||
const results = await seriesInStudy(qidoDicomWebClient, studyInstanceUid);
|
||||
|
||||
return processSeriesResults(results);
|
||||
},
|
||||
// processResults: processResults.bind(),
|
||||
},
|
||||
instances: {
|
||||
search: (studyInstanceUid, queryParameters) => {
|
||||
qidoDicomWebClient.headers = getAuthrorizationHeader();
|
||||
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
|
||||
*/
|
||||
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 = getAuthrorizationHeader();
|
||||
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._retrieveSeriesMetadataSync(
|
||||
StudyInstanceUID,
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction,
|
||||
madeInClient
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
store: {
|
||||
dicom: async (dataset, request, dicomDict) => {
|
||||
wadoDicomWebClient.headers = getAuthrorizationHeader();
|
||||
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);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
_retrieveSeriesMetadataSync: async (
|
||||
StudyInstanceUID,
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction,
|
||||
madeInClient
|
||||
) => {
|
||||
const enableStudyLazyLoad = false;
|
||||
wadoDicomWebClient.headers = generateWadoHeader();
|
||||
// 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 = 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)
|
||||
);
|
||||
|
||||
return seriesSummaryMetadata;
|
||||
},
|
||||
|
||||
_retrieveSeriesMetadataAsync: async (
|
||||
StudyInstanceUID,
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction,
|
||||
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) => {
|
||||
for (const key of Object.keys(naturalized)) {
|
||||
const value = naturalized[key];
|
||||
|
||||
if (Array.isArray(value) && typeof value[0] === 'object') {
|
||||
// Fix recursive values
|
||||
value.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 imageId = implementation.getImageIdsForInstance({
|
||||
instance,
|
||||
});
|
||||
|
||||
// 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
|
||||
instance.imageId = imageId;
|
||||
|
||||
// Adding UIDs to metadataProvider
|
||||
// Note: storing imageURI in metadataProvider since stack viewports
|
||||
// will use the same imageURI
|
||||
metadataProvider.addImageIdToUIDs(imageId, {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||
SOPInstanceUID: instance.SOPInstanceUID,
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
},
|
||||
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 = this.getImageIdsForInstance({
|
||||
instance,
|
||||
frame,
|
||||
});
|
||||
imageIds.push(imageId);
|
||||
}
|
||||
} else {
|
||||
const imageId = this.getImageIdsForInstance({ instance });
|
||||
imageIds.push(imageId);
|
||||
}
|
||||
});
|
||||
|
||||
return imageIds;
|
||||
},
|
||||
getImageIdsForInstance({ instance, frame = undefined }) {
|
||||
const imageIds = getImageId({
|
||||
instance,
|
||||
frame,
|
||||
config: dicomWebConfig,
|
||||
});
|
||||
return imageIds;
|
||||
},
|
||||
getConfig() {
|
||||
return dicomWebConfigCopy;
|
||||
},
|
||||
getStudyInstanceUIDs({ params, query }) {
|
||||
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params;
|
||||
const queryStudyInstanceUIDs = utils.splitComma(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);
|
||||
}
|
||||
|
||||
return IWebApiDataSource.create(implementation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
function retrieveBulkData(value, options = {}) {
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
export { createDicomWebApi };
|
||||
215
extensions/default/src/DicomWebDataSource/qido.js
Normal file
215
extensions/default/src/DicomWebDataSource/qido.js
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* QIDO - Query based on ID for DICOM Objects
|
||||
* search for studies, series and instances by patient ID, and receive their
|
||||
* unique identifiers for further usage.
|
||||
*
|
||||
* Quick: https://www.dicomstandard.org/dicomweb/query-qido-rs/
|
||||
* Standard: http://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.6
|
||||
*
|
||||
* Routes:
|
||||
* ==========
|
||||
* /studies?
|
||||
* /studies/{studyInstanceUid}/series?
|
||||
* /studies/{studyInstanceUid}/series/{seriesInstanceUid}/instances?
|
||||
*
|
||||
* Query Parameters:
|
||||
* ================
|
||||
* | KEY | VALUE |
|
||||
* |------------------|--------------------|
|
||||
* | {attributeId} | {value} |
|
||||
* | includeField | {attribute} or all |
|
||||
* | fuzzymatching | true OR false |
|
||||
* | limit | {number} |
|
||||
* | offset | {number} |
|
||||
*/
|
||||
import { DICOMWeb, utils } from '@ohif/core';
|
||||
import { sortStudySeries } from '@ohif/core/src/utils/sortStudy';
|
||||
|
||||
const { getString, getName, getModalities } = DICOMWeb;
|
||||
|
||||
/**
|
||||
* Parses resulting data from a QIDO call into a set of Study MetaData
|
||||
*
|
||||
* @param {Array} qidoStudies - An array of study objects. Each object contains a keys for DICOM tags.
|
||||
* @param {object} qidoStudies[0].qidoStudy - An object where each key is the DICOM Tag group+element
|
||||
* @param {object} qidoStudies[0].qidoStudy[dicomTag] - Optional object that represents DICOM Tag
|
||||
* @param {string} qidoStudies[0].qidoStudy[dicomTag].vr - Value Representation
|
||||
* @param {string[]} qidoStudies[0].qidoStudy[dicomTag].Value - Optional string array representation of the DICOM Tag's value
|
||||
* @returns {Array} An array of Study MetaData objects
|
||||
*/
|
||||
function processResults(qidoStudies) {
|
||||
if (!qidoStudies || !qidoStudies.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const studies = [];
|
||||
|
||||
qidoStudies.forEach(qidoStudy =>
|
||||
studies.push({
|
||||
studyInstanceUid: getString(qidoStudy['0020000D']),
|
||||
date: getString(qidoStudy['00080020']), // YYYYMMDD
|
||||
time: getString(qidoStudy['00080030']), // HHmmss.SSS (24-hour, minutes, seconds, fractional seconds)
|
||||
accession: getString(qidoStudy['00080050']) || '', // short string, probably a number?
|
||||
mrn: getString(qidoStudy['00100020']) || '', // medicalRecordNumber
|
||||
patientName: utils.formatPN(getName(qidoStudy['00100010'])) || '',
|
||||
instances: Number(getString(qidoStudy['00201208'])) || 0, // number
|
||||
description: getString(qidoStudy['00081030']) || '',
|
||||
modalities: getString(getModalities(qidoStudy['00080060'], qidoStudy['00080061'])) || '',
|
||||
})
|
||||
);
|
||||
|
||||
return studies;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 processSeriesResults(qidoSeries) {
|
||||
const series = [];
|
||||
|
||||
if (qidoSeries && qidoSeries.length) {
|
||||
qidoSeries.forEach(qidoSeries =>
|
||||
series.push({
|
||||
studyInstanceUid: getString(qidoSeries['0020000D']),
|
||||
seriesInstanceUid: getString(qidoSeries['0020000E']),
|
||||
modality: getString(qidoSeries['00080060']),
|
||||
seriesNumber: getString(qidoSeries['00200011']),
|
||||
seriesDate: utils.formatDate(getString(qidoSeries['00080021'])),
|
||||
numSeriesInstances: Number(getString(qidoSeries['00201209'])),
|
||||
description: getString(qidoSeries['0008103E']),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
sortStudySeries(series);
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} dicomWebClient - Client similar to what's provided by `dicomweb-client` library
|
||||
* @param {function} dicomWebClient.searchForStudies -
|
||||
* @param {string} [studyInstanceUid]
|
||||
* @param {string} [seriesInstanceUid]
|
||||
* @param {string} [queryParamaters]
|
||||
* @returns {Promise<results>} - Promise that resolves results
|
||||
*/
|
||||
async function search(dicomWebClient, studyInstanceUid, seriesInstanceUid, queryParameters) {
|
||||
let searchResult = await dicomWebClient.searchForStudies({
|
||||
studyInstanceUid: undefined,
|
||||
queryParams: queryParameters,
|
||||
});
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} studyInstanceUID - ID of study to return a list of series for
|
||||
* @returns {Promise} - Resolves SeriesMetadata[] in study
|
||||
*/
|
||||
export function seriesInStudy(dicomWebClient, studyInstanceUID) {
|
||||
// Series Description
|
||||
// Already included?
|
||||
const commaSeparatedFields = ['0008103E', '00080021'].join(',');
|
||||
const queryParams = {
|
||||
includefield: commaSeparatedFields,
|
||||
};
|
||||
|
||||
return dicomWebClient.searchForSeries({ studyInstanceUID, queryParams });
|
||||
}
|
||||
|
||||
export default function searchStudies(server, filter) {
|
||||
const queryParams = getQIDOQueryParams(filter, server.qidoSupportsIncludeField);
|
||||
const options = {
|
||||
queryParams,
|
||||
};
|
||||
|
||||
return dicomWeb.searchForStudies(options).then(resultDataToStudies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a QIDO URL given server details and a set of specified search filter
|
||||
* items
|
||||
*
|
||||
* @param filter
|
||||
* @param serverSupportsQIDOIncludeField
|
||||
* @returns {string} The URL with encoded filter query data
|
||||
*/
|
||||
function mapParams(params, options = {}) {
|
||||
if (!params) {
|
||||
return;
|
||||
}
|
||||
const commaSeparatedFields = [
|
||||
'00081030', // Study Description
|
||||
'00080060', // Modality
|
||||
// Add more fields here if you want them in the result
|
||||
].join(',');
|
||||
|
||||
const useWildcard =
|
||||
params?.disableWildcard !== undefined ? !params.disableWildcard : options.supportsWildcard;
|
||||
|
||||
const withWildcard = value => {
|
||||
return useWildcard && value ? `*${value}*` : value;
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
// Named
|
||||
PatientName: withWildcard(params.patientName),
|
||||
//PatientID: withWildcard(params.patientId),
|
||||
'00100020': withWildcard(params.patientId), // Temporarily to make the tests pass with dicomweb-server.. Apparently it's broken?
|
||||
AccessionNumber: withWildcard(params.accessionNumber),
|
||||
StudyDescription: withWildcard(params.studyDescription),
|
||||
ModalitiesInStudy: params.modalitiesInStudy,
|
||||
// Other
|
||||
limit: params.limit || 101,
|
||||
offset: params.offset || 0,
|
||||
fuzzymatching: options.supportsFuzzyMatching === true,
|
||||
includefield: commaSeparatedFields, // serverSupportsQIDOIncludeField ? commaSeparatedFields : 'all',
|
||||
};
|
||||
|
||||
// build the StudyDate range parameter
|
||||
if (params.startDate && params.endDate) {
|
||||
parameters.StudyDate = `${params.startDate}-${params.endDate}`;
|
||||
} else if (params.startDate) {
|
||||
const today = new Date();
|
||||
const DD = String(today.getDate()).padStart(2, '0');
|
||||
const MM = String(today.getMonth() + 1).padStart(2, '0'); //January is 0!
|
||||
const YYYY = today.getFullYear();
|
||||
const todayStr = `${YYYY}${MM}${DD}`;
|
||||
|
||||
parameters.StudyDate = `${params.startDate}-${todayStr}`;
|
||||
} else if (params.endDate) {
|
||||
const oldDateStr = `19700102`;
|
||||
|
||||
parameters.StudyDate = `${oldDateStr}-${params.endDate}`;
|
||||
}
|
||||
|
||||
// Build the StudyInstanceUID parameter
|
||||
if (params.studyInstanceUid) {
|
||||
let studyUids = params.studyInstanceUid;
|
||||
studyUids = Array.isArray(studyUids) ? studyUids.join() : studyUids;
|
||||
studyUids = studyUids.replace(/[^0-9.]+/g, '\\');
|
||||
parameters.StudyInstanceUID = studyUids;
|
||||
}
|
||||
|
||||
// Clean query params of undefined values.
|
||||
const final = {};
|
||||
Object.keys(parameters).forEach(key => {
|
||||
if (parameters[key] !== undefined && parameters[key] !== '') {
|
||||
final[key] = parameters[key];
|
||||
}
|
||||
});
|
||||
|
||||
return final;
|
||||
}
|
||||
|
||||
export { mapParams, search, processResults };
|
||||
@@ -0,0 +1,91 @@
|
||||
import retrieveMetadataFiltered from './utils/retrieveMetadataFiltered.js';
|
||||
import RetrieveMetadata from './wado/retrieveMetadata.js';
|
||||
|
||||
const moduleName = 'RetrieveStudyMetadata';
|
||||
// Cache for promises. Prevents unnecessary subsequent calls to the server
|
||||
const StudyMetaDataPromises = new Map();
|
||||
|
||||
/**
|
||||
* Retrieves study metadata.
|
||||
*
|
||||
* @param {Object} dicomWebClient The DICOMWebClient instance to be used for series load
|
||||
* @param {string} StudyInstanceUID The UID of the Study to be retrieved
|
||||
* @param {boolean} enableStudyLazyLoad Whether the study metadata should be loaded asynchronously.
|
||||
* @param {Object} [filters] Object containing filters to be applied on retrieve metadata process
|
||||
* @param {string} [filters.seriesInstanceUID] Series instance uid to filter results against
|
||||
* @param {function} [sortCriteria] Sort criteria function
|
||||
* @param {function} [sortFunction] Sort function
|
||||
*
|
||||
* @returns {Promise} that will be resolved with the metadata or rejected with the error
|
||||
*/
|
||||
export function retrieveStudyMetadata(
|
||||
dicomWebClient,
|
||||
StudyInstanceUID,
|
||||
enableStudyLazyLoad,
|
||||
filters,
|
||||
sortCriteria,
|
||||
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
|
||||
// corresponding promise from the "StudyMetaDataPromises" map...
|
||||
|
||||
if (!dicomWebClient) {
|
||||
throw new Error(`${moduleName}: Required 'dicomWebClient' parameter not provided.`);
|
||||
}
|
||||
if (!StudyInstanceUID) {
|
||||
throw new Error(`${moduleName}: Required 'StudyInstanceUID' parameter not provided.`);
|
||||
}
|
||||
|
||||
const promiseId = `${dicomWebConfig.name}:${StudyInstanceUID}`;
|
||||
|
||||
// Already waiting on result? Return cached promise
|
||||
if (StudyMetaDataPromises.has(promiseId)) {
|
||||
return StudyMetaDataPromises.get(promiseId);
|
||||
}
|
||||
|
||||
let promise;
|
||||
|
||||
if (filters && filters.seriesInstanceUID && Array.isArray(filters.seriesInstanceUID)) {
|
||||
promise = retrieveMetadataFiltered(
|
||||
dicomWebClient,
|
||||
StudyInstanceUID,
|
||||
enableStudyLazyLoad,
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction
|
||||
);
|
||||
} else {
|
||||
// Create a promise to handle the data retrieval
|
||||
promise = new Promise((resolve, reject) => {
|
||||
RetrieveMetadata(
|
||||
dicomWebClient,
|
||||
StudyInstanceUID,
|
||||
enableStudyLazyLoad,
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction
|
||||
).then(function (data) {
|
||||
resolve(data);
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
// Store the promise in cache
|
||||
StudyMetaDataPromises.set(promiseId, promise);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the cached study metadata retrieval promise to ensure that the browser will
|
||||
* re-retrieve the study metadata when it is next requested.
|
||||
*
|
||||
* @param {String} StudyInstanceUID The UID of the Study to be removed from cache
|
||||
*/
|
||||
export function deleteStudyMetadataPromise(StudyInstanceUID) {
|
||||
if (StudyMetaDataPromises.has(StudyInstanceUID)) {
|
||||
StudyMetaDataPromises.delete(StudyInstanceUID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import { api } from 'dicomweb-client';
|
||||
import fixMultipart from './fixMultipart';
|
||||
|
||||
const { DICOMwebClient } = api;
|
||||
|
||||
const anyDicomwebClient = DICOMwebClient as any;
|
||||
|
||||
// Ugly over-ride, but the internals aren't otherwise accessible.
|
||||
if (!anyDicomwebClient._orig_buildMultipartAcceptHeaderFieldValue) {
|
||||
anyDicomwebClient._orig_buildMultipartAcceptHeaderFieldValue =
|
||||
anyDicomwebClient._buildMultipartAcceptHeaderFieldValue;
|
||||
anyDicomwebClient._buildMultipartAcceptHeaderFieldValue = function (mediaTypes, acceptableTypes) {
|
||||
if (mediaTypes.length === 1 && mediaTypes[0].mediaType.endsWith('/*')) {
|
||||
return '*/*';
|
||||
} else {
|
||||
return anyDicomwebClient._orig_buildMultipartAcceptHeaderFieldValue(
|
||||
mediaTypes,
|
||||
acceptableTypes
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation of the static wado client, that fetches data from
|
||||
* a static response rather than actually doing real queries. This allows
|
||||
* fast encoding of test data, but because it is static, anything actually
|
||||
* 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',
|
||||
patientname: '00100010',
|
||||
'00100020': 'mrn',
|
||||
studydescription: '00081030',
|
||||
studydate: '00080020',
|
||||
modalitiesinstudy: '00080061',
|
||||
accessionnumber: '00080050',
|
||||
};
|
||||
|
||||
static seriesFilterKeys = {
|
||||
seriesinstanceuid: '0020000E',
|
||||
seriesnumber: '00200011',
|
||||
modality: '00080060',
|
||||
};
|
||||
|
||||
protected config;
|
||||
protected staticWado;
|
||||
|
||||
constructor(config) {
|
||||
super(config);
|
||||
this.staticWado = config.staticWado;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle improperly specified multipart/related return type.
|
||||
* Note if the response is SUPPOSED to be multipart encoded already, then this
|
||||
* will double-decode it.
|
||||
*
|
||||
* @param options
|
||||
* @returns De-multiparted response data.
|
||||
*
|
||||
*/
|
||||
public retrieveBulkData(options): Promise<any[]> {
|
||||
const shouldFixMultipart = this.config.fixBulkdataMultipart !== false;
|
||||
const useOptions = {
|
||||
...options,
|
||||
};
|
||||
if (this.staticWado) {
|
||||
useOptions.mediaTypes = [{ mediaType: 'application/*' }];
|
||||
}
|
||||
return super
|
||||
.retrieveBulkData(useOptions)
|
||||
.then(result => (shouldFixMultipart ? fixMultipart(result) : result));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves instance frames using the image/* media type when configured
|
||||
* to do so (static wado back end).
|
||||
*/
|
||||
public retrieveInstanceFrames(options) {
|
||||
if (this.staticWado) {
|
||||
return super.retrieveInstanceFrames({
|
||||
...options,
|
||||
mediaTypes: [{ mediaType: 'image/*' }],
|
||||
});
|
||||
} else {
|
||||
return super.retrieveInstanceFrames(options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the search for studies remote query with a local version which
|
||||
* retrieves a complete query list and then sub-selects from it locally.
|
||||
* @param {*} options
|
||||
* @returns
|
||||
*/
|
||||
async searchForStudies(options) {
|
||||
if (!this.staticWado) {
|
||||
return super.searchForStudies(options);
|
||||
}
|
||||
|
||||
const searchResult = await super.searchForStudies(options);
|
||||
const { queryParams } = options;
|
||||
|
||||
if (!queryParams) {
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
const lowerParams = this.toLowerParams(queryParams);
|
||||
const filtered = searchResult.filter(study => {
|
||||
for (const key of Object.keys(StaticWadoClient.studyFilterKeys)) {
|
||||
if (!this.filterItem(key, lowerParams, study, StaticWadoClient.studyFilterKeys)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return filtered;
|
||||
}
|
||||
|
||||
async searchForSeries(options) {
|
||||
if (!this.staticWado) {
|
||||
return super.searchForSeries(options);
|
||||
}
|
||||
|
||||
const searchResult = await super.searchForSeries(options);
|
||||
const { queryParams } = options;
|
||||
if (!queryParams) {
|
||||
return searchResult;
|
||||
}
|
||||
const lowerParams = this.toLowerParams(queryParams);
|
||||
|
||||
const filtered = searchResult.filter(series => {
|
||||
for (const key of Object.keys(StaticWadoClient.seriesFilterKeys)) {
|
||||
if (!this.filterItem(key, lowerParams, series, StaticWadoClient.seriesFilterKeys)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares values, matching any instance of desired to any instance of
|
||||
* actual by recursively go through the paired set of values. That is,
|
||||
* this is O(m*n) where m is how many items in desired and n is the length of actual
|
||||
* Then, at the individual item node, compares the Alphabetic name if present,
|
||||
* and does a sub-string matching on string values, and otherwise does an
|
||||
* exact match comparison.
|
||||
*
|
||||
* @param {*} desired
|
||||
* @param {*} actual
|
||||
* @returns true if the values match
|
||||
*/
|
||||
compareValues(desired, actual) {
|
||||
if (Array.isArray(desired)) {
|
||||
return desired.find(item => this.compareValues(item, actual));
|
||||
}
|
||||
if (Array.isArray(actual)) {
|
||||
return actual.find(actualItem => this.compareValues(desired, actualItem));
|
||||
}
|
||||
if (actual?.Alphabetic) {
|
||||
actual = actual.Alphabetic;
|
||||
}
|
||||
if (typeof actual == 'string') {
|
||||
if (actual.length === 0) {
|
||||
return true;
|
||||
}
|
||||
if (desired.length === 0 || desired === '*') {
|
||||
return true;
|
||||
}
|
||||
if (desired[0] === '*' && desired[desired.length - 1] === '*') {
|
||||
// console.log(`Comparing ${actual} to ${desired.substring(1, desired.length - 1)}`)
|
||||
return actual.indexOf(desired.substring(1, desired.length - 1)) != -1;
|
||||
} else if (desired[desired.length - 1] === '*') {
|
||||
return actual.indexOf(desired.substring(0, desired.length - 1)) != -1;
|
||||
} else if (desired[0] === '*') {
|
||||
return actual.indexOf(desired.substring(1)) === actual.length - desired.length + 1;
|
||||
}
|
||||
}
|
||||
return desired === actual;
|
||||
}
|
||||
|
||||
/** Compares a pair of dates to see if the value is within the range */
|
||||
compareDateRange(range, value) {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
const dash = range.indexOf('-');
|
||||
if (dash === -1) {
|
||||
return this.compareValues(range, value);
|
||||
}
|
||||
const start = range.substring(0, dash);
|
||||
const end = range.substring(dash + 1);
|
||||
return (!start || value >= start) && (!end || value <= end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the return list by the query parameters.
|
||||
*
|
||||
* @param anyCaseKey - a possible search key
|
||||
* @param queryParams -
|
||||
* @param {*} study
|
||||
* @param {*} sourceFilterMap
|
||||
* @returns
|
||||
*/
|
||||
filterItem(key: string, queryParams, study, sourceFilterMap) {
|
||||
const altKey = sourceFilterMap[key] || key;
|
||||
if (!queryParams) {
|
||||
return true;
|
||||
}
|
||||
const testValue = queryParams[key] || queryParams[altKey];
|
||||
if (!testValue) {
|
||||
return true;
|
||||
}
|
||||
const valueElem = study[key] || study[altKey];
|
||||
if (!valueElem) {
|
||||
return false;
|
||||
}
|
||||
if (valueElem.vr === 'DA' && valueElem.Value?.[0]) {
|
||||
return this.compareDateRange(testValue, valueElem.Value[0]);
|
||||
}
|
||||
const value = valueElem.Value;
|
||||
return this.compareValues(testValue, value);
|
||||
}
|
||||
|
||||
/** Converts the query parameters to lower case query parameters */
|
||||
toLowerParams(queryParams: Record<string, unknown>): Record<string, unknown> {
|
||||
const lowerParams = {};
|
||||
Object.entries(queryParams).forEach(([key, value]) => {
|
||||
lowerParams[key.toLowerCase()] = value;
|
||||
});
|
||||
return lowerParams;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { fixBulkDataURI } from './fixBulkDataURI';
|
||||
|
||||
function isPrimitive(v: any) {
|
||||
return !(typeof v == 'object' || Array.isArray(v));
|
||||
}
|
||||
|
||||
const vrNumerics = new Set([
|
||||
'DS',
|
||||
'FL',
|
||||
'FD',
|
||||
'IS',
|
||||
'OD',
|
||||
'OF',
|
||||
'OL',
|
||||
'OV',
|
||||
'SL',
|
||||
'SS',
|
||||
'SV',
|
||||
'UL',
|
||||
'US',
|
||||
'UV',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Specialized for DICOM JSON format dataset cleaning.
|
||||
* @param obj
|
||||
* @returns
|
||||
*/
|
||||
export function cleanDenaturalizedDataset(
|
||||
obj: any,
|
||||
options?: {
|
||||
StudyInstanceUID: string;
|
||||
SeriesInstanceUID: string;
|
||||
dataSourceConfig: unknown;
|
||||
}
|
||||
): any {
|
||||
if (Array.isArray(obj)) {
|
||||
const newAry = obj.map(o => (isPrimitive(o) ? o : cleanDenaturalizedDataset(o, options)));
|
||||
return newAry;
|
||||
}
|
||||
if (isPrimitive(obj)) {
|
||||
return obj;
|
||||
}
|
||||
Object.keys(obj).forEach(key => {
|
||||
if (obj[key].Value === null && obj[key].vr) {
|
||||
delete obj[key].Value;
|
||||
} else if (Array.isArray(obj[key].Value) && obj[key].vr) {
|
||||
if (obj[key].Value.length === 1 && obj[key].Value[0].BulkDataURI) {
|
||||
if (options?.dataSourceConfig) {
|
||||
// Not needed unless data source is directly used for loading data.
|
||||
fixBulkDataURI(obj[key].Value[0], options, options.dataSourceConfig);
|
||||
}
|
||||
|
||||
obj[key].BulkDataURI = obj[key].Value[0].BulkDataURI;
|
||||
|
||||
// prevent mixed-content blockage
|
||||
if (window.location.protocol === 'https:' && obj[key].BulkDataURI.startsWith('http:')) {
|
||||
obj[key].BulkDataURI = obj[key].BulkDataURI.replace('http:', 'https:');
|
||||
}
|
||||
delete obj[key].Value;
|
||||
} else if (vrNumerics.has(obj[key].vr)) {
|
||||
obj[key].Value = obj[key].Value.map(v => +v);
|
||||
} else {
|
||||
obj[key].Value = obj[key].Value.map(entry => cleanDenaturalizedDataset(entry, options));
|
||||
}
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is required to make the denaturalized data transferrable when it has
|
||||
* added proxy values.
|
||||
*/
|
||||
export function transferDenaturalizedDataset(dataset) {
|
||||
const noNull = cleanDenaturalizedDataset(dataset);
|
||||
return JSON.parse(JSON.stringify(noNull));
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
function checkToken(token, data, dataOffset): boolean {
|
||||
if (dataOffset + token.length > data.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let endIndex = dataOffset;
|
||||
|
||||
for (let i = 0; i < token.length; i++) {
|
||||
if (token[i] !== data[endIndex++]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function stringToUint8Array(str: string): Uint8Array {
|
||||
const uint = new Uint8Array(str.length);
|
||||
|
||||
for (let i = 0, j = str.length; i < j; i++) {
|
||||
uint[i] = str.charCodeAt(i);
|
||||
}
|
||||
|
||||
return uint;
|
||||
}
|
||||
|
||||
function findIndexOfString(
|
||||
data: Uint8Array,
|
||||
str: string,
|
||||
offset?: number
|
||||
): number {
|
||||
offset = offset || 0;
|
||||
|
||||
const token = stringToUint8Array(str);
|
||||
|
||||
for (let i = offset; i < data.length; i++) {
|
||||
if (token[0] === data[i]) {
|
||||
// console.log('match @', i);
|
||||
if (checkToken(token, data, i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
export default findIndexOfString;
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Modifies a bulkDataURI to ensure it is absolute based on the DICOMWeb configuration and
|
||||
* instance data. The modification is in-place.
|
||||
*
|
||||
* If the bulkDataURI is relative to the series or study (according to the DICOM standard),
|
||||
* it is made absolute by prepending the relevant paths.
|
||||
*
|
||||
* In scenarios where the bulkDataURI is a server-relative path (starting with '/'), the function
|
||||
* handles two cases:
|
||||
*
|
||||
* 1. If the wado root is absolute (starts with 'http'), it prepends the wado root to the bulkDataURI.
|
||||
* 2. If the wado root is relative, no changes are needed as the bulkDataURI is already correctly relative to the server root.
|
||||
*
|
||||
* @param value - The object containing BulkDataURI to be fixed.
|
||||
* @param instance - The object (DICOM instance data) containing StudyInstanceUID and SeriesInstanceUID.
|
||||
* @param dicomWebConfig - The DICOMWeb configuration object, containing wadoRoot and potentially bulkDataURI.relativeResolution.
|
||||
* @returns The function modifies `value` in-place, it does not return a value.
|
||||
*/
|
||||
function fixBulkDataURI(value, instance, dicomWebConfig) {
|
||||
// in case of the relative path, make it absolute. The current DICOM standard says
|
||||
// the bulkdataURI is relative to the series. However, there are situations where
|
||||
// it can be relative to the study too
|
||||
let { BulkDataURI } = value;
|
||||
const { bulkDataURI: uriConfig = {} } = dicomWebConfig;
|
||||
|
||||
BulkDataURI = uriConfig.transform?.(BulkDataURI) || BulkDataURI;
|
||||
|
||||
// Handle incorrectly prefixed origins
|
||||
const { startsWith, prefixWith = '' } = uriConfig;
|
||||
if (startsWith && BulkDataURI.startsWith(startsWith)) {
|
||||
BulkDataURI = prefixWith + BulkDataURI.substring(startsWith.length);
|
||||
value.BulkDataURI = BulkDataURI;
|
||||
}
|
||||
|
||||
if (!BulkDataURI.startsWith('http') && !value.BulkDataURI.startsWith('/')) {
|
||||
const { StudyInstanceUID, SeriesInstanceUID } = instance;
|
||||
const isInstanceStart = BulkDataURI.startsWith('instances/') || BulkDataURI.startsWith('../');
|
||||
if (
|
||||
BulkDataURI.startsWith('series/') ||
|
||||
BulkDataURI.startsWith('bulkdata/') ||
|
||||
(uriConfig.relativeResolution === 'studies' && !isInstanceStart)
|
||||
) {
|
||||
value.BulkDataURI = `${dicomWebConfig.wadoRoot}/studies/${StudyInstanceUID}/${BulkDataURI}`;
|
||||
} else if (
|
||||
isInstanceStart ||
|
||||
uriConfig.relativeResolution === 'series' ||
|
||||
!uriConfig.relativeResolution
|
||||
) {
|
||||
value.BulkDataURI = `${dicomWebConfig.wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/${BulkDataURI}`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// in case it is relative path but starts at the server (e.g., /bulk/1e, note the missing http
|
||||
// in the beginning and the first character is /) There are two scenarios, whether the wado root
|
||||
// is absolute or relative. In case of absolute, we need to prepend the wado root to the bulkdata
|
||||
// uri (e.g., bulkData: /bulk/1e, wado root: http://myserver.com/dicomweb, output: http://myserver.com/bulk/1e)
|
||||
// and in case of relative wado root, we need to prepend the bulkdata uri to the wado root (e.g,. bulkData: /bulk/1e
|
||||
// wado root: /dicomweb, output: /bulk/1e)
|
||||
if (BulkDataURI[0] === '/') {
|
||||
if (dicomWebConfig.wadoRoot.startsWith('http')) {
|
||||
// Absolute wado root
|
||||
const url = new URL(dicomWebConfig.wadoRoot);
|
||||
value.BulkDataURI = `${url.origin}${BulkDataURI}`;
|
||||
} else {
|
||||
// Relative wado root, we don't need to do anything, bulkdata uri is already correct
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { fixBulkDataURI };
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Fix multi-valued keys so that those which are strings split by
|
||||
* a backslash are returned as arrays.
|
||||
*/
|
||||
export function fixMultiValueKeys(naturalData, keys = ['ImageType']) {
|
||||
for (const key of keys) {
|
||||
if (typeof naturalData[key] === 'string') {
|
||||
naturalData[key] = naturalData[key].split('\\');
|
||||
}
|
||||
}
|
||||
return naturalData;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import findIndexOfString from './findIndexOfString';
|
||||
|
||||
/**
|
||||
* Fix multipart data coming back from the retrieve bulkdata request, but
|
||||
* incorrectly tagged as application/octet-stream. Some servers don't handle
|
||||
* the response type correctly, and this method is relatively robust about
|
||||
* detecting multipart data correctly. It will only extract one value.
|
||||
*/
|
||||
export default function fixMultipart(arrayData) {
|
||||
const data = new Uint8Array(arrayData[0]);
|
||||
// Don't know the exact minimum length, but it is at least 25 to encode multipart
|
||||
if (data.length < 25) {
|
||||
return arrayData;
|
||||
}
|
||||
const dashIndex = findIndexOfString(data, '--');
|
||||
if (dashIndex > 6) {
|
||||
return arrayData;
|
||||
}
|
||||
const tokenIndex = findIndexOfString(data, '\r\n\r\n', dashIndex);
|
||||
if (tokenIndex > 512) {
|
||||
// Allow for 512 characters in the header - there is no apriori limit, but
|
||||
// this seems ok for now as we only expect it to have content type in it.
|
||||
return arrayData;
|
||||
}
|
||||
const header = uint8ArrayToString(data, 0, tokenIndex);
|
||||
// Now find the boundary marker
|
||||
const responseHeaders = header.split('\r\n');
|
||||
const boundary = findBoundary(responseHeaders);
|
||||
|
||||
if (!boundary) {
|
||||
return arrayData;
|
||||
}
|
||||
// Start of actual data is 4 characters after the token
|
||||
const offset = tokenIndex + 4;
|
||||
|
||||
const endIndex = findIndexOfString(data, boundary, offset);
|
||||
if (endIndex === -1) {
|
||||
return arrayData;
|
||||
}
|
||||
|
||||
return [data.slice(offset, endIndex - 2).buffer];
|
||||
}
|
||||
|
||||
export function findBoundary(header: string[]): string {
|
||||
for (let i = 0; i < header.length; i++) {
|
||||
if (header[i].substr(0, 2) === '--') {
|
||||
return header[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function findContentType(header: string[]): string {
|
||||
for (let i = 0; i < header.length; i++) {
|
||||
if (header[i].substr(0, 13) === 'Content-Type:') {
|
||||
return header[i].substr(13).trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function uint8ArrayToString(data, offset, length) {
|
||||
offset = offset || 0;
|
||||
length = length || data.length - offset;
|
||||
let str = '';
|
||||
|
||||
for (let i = offset; i < offset + length; i++) {
|
||||
str += String.fromCharCode(data[i]);
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import getWADORSImageId from './getWADORSImageId';
|
||||
|
||||
function buildInstanceWadoUrl(config, instance) {
|
||||
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance;
|
||||
const params = [];
|
||||
|
||||
params.push('requestType=WADO');
|
||||
params.push(`studyUID=${StudyInstanceUID}`);
|
||||
params.push(`seriesUID=${SeriesInstanceUID}`);
|
||||
params.push(`objectUID=${SOPInstanceUID}`);
|
||||
params.push('contentType=application/dicom');
|
||||
params.push('transferSyntax=*');
|
||||
|
||||
const paramString = params.join('&');
|
||||
|
||||
return `${config.wadoUriRoot}?${paramString}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain an imageId for Cornerstone from an image instance
|
||||
*
|
||||
* @param instance
|
||||
* @param frame
|
||||
* @param thumbnail
|
||||
* @returns {string} The imageId to be used by Cornerstone
|
||||
*/
|
||||
export default function getImageId({ instance, frame, config, thumbnail = false }) {
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (instance.imageId && frame === undefined) {
|
||||
return instance.imageId;
|
||||
}
|
||||
|
||||
if (instance.url) {
|
||||
return instance.url;
|
||||
}
|
||||
|
||||
const renderingAttr = thumbnail ? 'thumbnailRendering' : 'imageRendering';
|
||||
|
||||
if (!config[renderingAttr] || config[renderingAttr] === 'wadouri') {
|
||||
const wadouri = buildInstanceWadoUrl(config, instance);
|
||||
|
||||
let imageId = 'dicomweb:' + wadouri;
|
||||
if (frame !== undefined) {
|
||||
imageId += '&frame=' + frame;
|
||||
}
|
||||
|
||||
return imageId;
|
||||
} else {
|
||||
return getWADORSImageId(instance, config, frame); // WADO-RS Retrieve Frame
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
function buildInstanceWadoRsUri(instance, config) {
|
||||
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance;
|
||||
return `${config.wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}`;
|
||||
}
|
||||
|
||||
function buildInstanceFrameWadoRsUri(instance, config, frame) {
|
||||
const baseWadoRsUri = buildInstanceWadoRsUri(instance, config);
|
||||
|
||||
frame = frame || 1;
|
||||
|
||||
return `${baseWadoRsUri}/frames/${frame}`;
|
||||
}
|
||||
|
||||
// function getWADORSImageUrl(instance, frame) {
|
||||
// const wadorsuri = buildInstanceFrameWadoRsUri(instance, config, frame);
|
||||
|
||||
// if (!wadorsuri) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Use null to obtain an imageId which represents the instance
|
||||
// if (frame === null) {
|
||||
// wadorsuri = wadorsuri.replace(/frames\/(\d+)/, '');
|
||||
// } else {
|
||||
// // We need to sum 1 because WADO-RS frame number is 1-based
|
||||
// frame = frame ? parseInt(frame) + 1 : 1;
|
||||
|
||||
// // Replaces /frame/1 by /frame/{frame}
|
||||
// wadorsuri = wadorsuri.replace(/frames\/(\d+)/, `frames/${frame}`);
|
||||
// }
|
||||
|
||||
// return wadorsuri;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Obtain an imageId for Cornerstone based on the WADO-RS scheme
|
||||
*
|
||||
* @param {object} instanceMetada metadata object (InstanceMetadata)
|
||||
* @param {(string\|number)} [frame] the frame number
|
||||
* @returns {string} The imageId to be used by Cornerstone
|
||||
*/
|
||||
export default function getWADORSImageId(instance, config, frame) {
|
||||
//const uri = getWADORSImageUrl(instance, frame);
|
||||
const uri = buildInstanceFrameWadoRsUri(instance, config, frame);
|
||||
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
|
||||
return `wadors:${uri}`;
|
||||
}
|
||||
9
extensions/default/src/DicomWebDataSource/utils/index.ts
Normal file
9
extensions/default/src/DicomWebDataSource/utils/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { fixBulkDataURI } from './fixBulkDataURI';
|
||||
import {
|
||||
cleanDenaturalizedDataset,
|
||||
transferDenaturalizedDataset,
|
||||
} from './cleanDenaturalizedDataset';
|
||||
|
||||
export { fixMultiValueKeys } from './fixMultiValueKeys';
|
||||
|
||||
export { fixBulkDataURI, cleanDenaturalizedDataset, transferDenaturalizedDataset };
|
||||
@@ -0,0 +1,61 @@
|
||||
import RetrieveMetadata from '../wado/retrieveMetadata';
|
||||
|
||||
/**
|
||||
* Retrieve metadata filtered.
|
||||
*
|
||||
* @param {*} dicomWebClient The DICOMWebClient instance to be used for series load
|
||||
* @param {*} StudyInstanceUID The UID of the Study to be retrieved
|
||||
* @param {*} enableStudyLazyLoad Whether the study metadata should be loaded asynchronously
|
||||
* @param {object} filters Object containing filters to be applied on retrieve metadata process
|
||||
* @param {string} [filters.seriesInstanceUID] Series instance uid to filter results against
|
||||
* @param {function} [sortCriteria] Sort criteria function
|
||||
* @param {function} [sortFunction] Sort function
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
function retrieveMetadataFiltered(
|
||||
dicomWebClient,
|
||||
StudyInstanceUID,
|
||||
enableStudyLazyLoad,
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction
|
||||
) {
|
||||
const { seriesInstanceUID } = filters;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const promises = seriesInstanceUID.map(uid => {
|
||||
const seriesSpecificFilters = Object.assign({}, filters, {
|
||||
seriesInstanceUID: uid,
|
||||
});
|
||||
|
||||
return RetrieveMetadata(
|
||||
dicomWebClient,
|
||||
StudyInstanceUID,
|
||||
enableStudyLazyLoad,
|
||||
seriesSpecificFilters,
|
||||
sortCriteria,
|
||||
sortFunction
|
||||
);
|
||||
});
|
||||
|
||||
if (enableStudyLazyLoad === true) {
|
||||
Promise.all(promises).then(results => {
|
||||
const aggregatedResult = { preLoadData: [], promises: [] };
|
||||
|
||||
results.forEach(({ preLoadData, promises }) => {
|
||||
aggregatedResult.preLoadData = aggregatedResult.preLoadData.concat(preLoadData);
|
||||
aggregatedResult.promises = aggregatedResult.promises.concat(promises);
|
||||
});
|
||||
|
||||
resolve(aggregatedResult);
|
||||
}, reject);
|
||||
} else {
|
||||
Promise.all(promises).then(results => {
|
||||
resolve(results.flat());
|
||||
}, reject);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default retrieveMetadataFiltered;
|
||||
@@ -0,0 +1,41 @@
|
||||
import RetrieveMetadataLoaderSync from './retrieveMetadataLoaderSync';
|
||||
import RetrieveMetadataLoaderAsync from './retrieveMetadataLoaderAsync';
|
||||
|
||||
/**
|
||||
* Retrieve Study metadata from a DICOM server. If the server is configured to use lazy load, only the first series
|
||||
* will be loaded and the property "studyLoader" will be set to let consumer load remaining series as needed.
|
||||
*
|
||||
* @param {*} dicomWebClient The DICOMWebClient instance to be used for series load
|
||||
* @param {*} StudyInstanceUID The UID of the Study to be retrieved
|
||||
* @param {*} enableStudyLazyLoad Whether the study metadata should be loaded asynchronously
|
||||
* @param {object} filters Object containing filters to be applied on retrieve metadata process
|
||||
* @param {string} [filters.seriesInstanceUID] Series instance uid to filter results against
|
||||
* @param {function} [sortCriteria] Sort criteria function
|
||||
* @param {function} [sortFunction] Sort function
|
||||
*
|
||||
* @returns {Promise} A promises that resolves the study descriptor object
|
||||
*/
|
||||
async function RetrieveMetadata(
|
||||
dicomWebClient,
|
||||
StudyInstanceUID,
|
||||
enableStudyLazyLoad,
|
||||
filters = {},
|
||||
sortCriteria,
|
||||
sortFunction
|
||||
) {
|
||||
const RetrieveMetadataLoader =
|
||||
enableStudyLazyLoad !== false ? RetrieveMetadataLoaderAsync : RetrieveMetadataLoaderSync;
|
||||
|
||||
const retrieveMetadataLoader = new RetrieveMetadataLoader(
|
||||
dicomWebClient,
|
||||
StudyInstanceUID,
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction
|
||||
);
|
||||
const data = await retrieveMetadataLoader.execLoad();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export default RetrieveMetadata;
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Class to define inheritance of load retrieve strategy.
|
||||
* The process can be async load (lazy) or sync load
|
||||
*
|
||||
* There are methods that must be implemented at consumer level
|
||||
* To retrieve study call execLoad
|
||||
*/
|
||||
export default class RetrieveMetadataLoader {
|
||||
/**
|
||||
* @constructor
|
||||
* @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} [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 = undefined,
|
||||
sortFunction = undefined
|
||||
) {
|
||||
this.client = client;
|
||||
this.studyInstanceUID = studyInstanceUID;
|
||||
this.filters = filters;
|
||||
this.sortCriteria = sortCriteria;
|
||||
this.sortFunction = sortFunction;
|
||||
}
|
||||
|
||||
async execLoad() {
|
||||
const preLoadData = await this.preLoad();
|
||||
const loadData = await this.load(preLoadData);
|
||||
const postLoadData = await this.posLoad(loadData);
|
||||
return postLoadData;
|
||||
}
|
||||
|
||||
/**
|
||||
* It iterates over given loaders running each one. Loaders parameters must be bind when getting it.
|
||||
* @param {Array} loaders - array of loader to retrieve data.
|
||||
*/
|
||||
async runLoaders(loaders) {
|
||||
let result;
|
||||
for (const loader of loaders) {
|
||||
result = await loader();
|
||||
if (result && result.length) {
|
||||
break; // closes iterator in case data is retrieved successfully
|
||||
}
|
||||
}
|
||||
|
||||
if (loaders.next().done && !result) {
|
||||
throw new Error('RetrieveMetadataLoader failed');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Methods to be overwrite
|
||||
async configLoad() {}
|
||||
async preLoad() {}
|
||||
async load(preLoadData) {}
|
||||
async posLoad(loadData) {}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import dcmjs from 'dcmjs';
|
||||
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.
|
||||
*
|
||||
* @param {DICOMWebClient} dicomWebClient The DICOMWebClient instance to be used for series load
|
||||
* @param {string} studyInstanceUID The Study Instance UID from which series will be loaded
|
||||
* @param {Array} seriesInstanceUIDList A list of Series Instance UIDs
|
||||
*
|
||||
* @returns {Object} Returns an object which supports loading of instances from each of given Series Instance UID
|
||||
*/
|
||||
function makeSeriesAsyncLoader(client, studyInstanceUID, seriesInstanceUIDList) {
|
||||
return Object.freeze({
|
||||
hasNext() {
|
||||
return seriesInstanceUIDList.length > 0;
|
||||
},
|
||||
next() {
|
||||
const { seriesInstanceUID, metadata } = seriesInstanceUIDList.shift();
|
||||
const promise = new DeferredPromise();
|
||||
promise.setMetadata(metadata);
|
||||
promise.setProcessFunction(() => {
|
||||
return client.retrieveSeriesMetadata({
|
||||
studyInstanceUID,
|
||||
seriesInstanceUID,
|
||||
});
|
||||
});
|
||||
return promise;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for async load of study metadata.
|
||||
* It inherits from RetrieveMetadataLoader
|
||||
*
|
||||
* It loads the one series and then append to seriesLoader the others to be consumed/loaded
|
||||
*/
|
||||
export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader {
|
||||
/**
|
||||
* @returns {Array} Array of preLoaders. To be consumed as queue
|
||||
*/
|
||||
*getPreLoaders() {
|
||||
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) {
|
||||
options.queryParams.SeriesInstanceUID = seriesInstanceUID;
|
||||
preLoaders.push(client.searchForSeries.bind(client, options));
|
||||
}
|
||||
// Fallback preloader
|
||||
preLoaders.push(client.searchForSeries.bind(client, options));
|
||||
|
||||
yield* preLoaders;
|
||||
}
|
||||
|
||||
async preLoad() {
|
||||
const preLoaders = this.getPreLoaders();
|
||||
const result = await this.runLoaders(preLoaders);
|
||||
const sortCriteria = this.sortCriteria;
|
||||
const sortFunction = this.sortFunction;
|
||||
|
||||
const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary;
|
||||
const naturalized = result.map(naturalizeDataset);
|
||||
|
||||
return sortStudySeries(naturalized, sortCriteria, sortFunction);
|
||||
}
|
||||
|
||||
async load(preLoadData) {
|
||||
const { client, studyInstanceUID } = this;
|
||||
|
||||
const seriesInstanceUIDs = preLoadData.map(seriesMetadata => {
|
||||
return { seriesInstanceUID: seriesMetadata.SeriesInstanceUID, metadata: seriesMetadata };
|
||||
});
|
||||
|
||||
const seriesAsyncLoader = makeSeriesAsyncLoader(client, studyInstanceUID, seriesInstanceUIDs);
|
||||
|
||||
const promises = [];
|
||||
|
||||
while (seriesAsyncLoader.hasNext()) {
|
||||
const promise = seriesAsyncLoader.next();
|
||||
promises.push(promise);
|
||||
}
|
||||
|
||||
return {
|
||||
preLoadData,
|
||||
promises,
|
||||
};
|
||||
}
|
||||
|
||||
async posLoad({ preLoadData, promises }) {
|
||||
return {
|
||||
preLoadData,
|
||||
promises,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// import { api } from 'dicomweb-client';
|
||||
// import DICOMWeb from '../../../DICOMWeb/';
|
||||
import { createStudyFromSOPInstanceList } from './studyInstanceHelpers';
|
||||
import RetrieveMetadataLoader from './retrieveMetadataLoader';
|
||||
|
||||
/**
|
||||
* Class for sync load of study metadata.
|
||||
* It inherits from RetrieveMetadataLoader
|
||||
*
|
||||
* A list of loaders (getLoaders) can be created so, it will be applied a fallback load strategy.
|
||||
* 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;
|
||||
|
||||
if (seriesInstanceUID) {
|
||||
loaders.push(
|
||||
client.retrieveSeriesMetadata.bind(client, {
|
||||
studyInstanceUID,
|
||||
seriesInstanceUID,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
loaders.push(client.retrieveStudyMetadata.bind(client, { studyInstanceUID }));
|
||||
|
||||
yield* loaders;
|
||||
}
|
||||
|
||||
async load(preLoadData) {
|
||||
const loaders = this.getLoaders();
|
||||
const result = this.runLoaders(loaders);
|
||||
return result;
|
||||
}
|
||||
|
||||
async posLoad(loadData) {
|
||||
return loadData;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user