diff --git a/extensions/default/package.json b/extensions/default/package.json index 6071aea84..e9633cdc1 100644 --- a/extensions/default/package.json +++ b/extensions/default/package.json @@ -31,7 +31,8 @@ "prop-types": "^15.6.2", "react": "^16.13.1", "react-dom": "^16.13.1", - "webpack": "^4.0.0" + "webpack": "^4.0.0", + "dcmjs": "^0.12.4" }, "dependencies": { "@babel/runtime": "7.7.6" diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index a25181556..1771aa79d 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -1,6 +1,291 @@ import { api } from 'dicomweb-client'; import { mapParams, search as qidoSearch, processResults } from './qido.js'; -import { IWebApiDataSource } from '@ohif/core'; +import { dicomMetadataStore, IWebApiDataSource, utils } from '@ohif/core'; +import * as dcmjs from 'dcmjs'; +//import { retrieveStudyMetadata } from './retrieveStudyMetadata.js'; + +const { urlUtil } = utils; + +const exampleInstances = [ + { + '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', + }, + }, +]; /** * @@ -14,13 +299,21 @@ import { IWebApiDataSource } from '@ohif/core'; * @param {bool} lazyLoadStudy - "enableStudyLazyLoad"; Request series meta async instead of blocking */ function createDicomWebApi(dicomWebConfig) { - const { qidoRoot } = dicomWebConfig; - const config = { + const { qidoRoot, wadoRoot, enableStudyLazyLoad } = dicomWebConfig; + + const qidoConfig = { url: qidoRoot, // headers: DICOMWeb.getAuthorizationHeader(server), }; - const dicomWebClient = new api.DICOMwebClient(config); + const wadoConfig = { + url: wadoRoot, + }; + + // TODO -> Two clients sucks, but its better than 1000. + // TODO -> We'll need to merge auth later. + const qidoDicomWebClient = new api.DICOMwebClient(qidoConfig); + const wadoDicomWebClient = new api.DICOMwebClient(wadoConfig); return IWebApiDataSource.create({ query: { @@ -31,7 +324,7 @@ function createDicomWebApi(dicomWebConfig) { mapParams(origParams) || {}; const results = await qidoSearch( - dicomWebClient, + qidoDicomWebClient, studyInstanceUid, seriesInstanceUid, mappedParams @@ -45,13 +338,47 @@ function createDicomWebApi(dicomWebConfig) { search: (studyInstanceUid, queryParamaters) => qidoSearch.call( undefined, - dicomWebClient, + qidoDicomWebClient, studyInstanceUid, null, queryParamaters ), }, }, + retrieve: { + series: { + metadata: (queryParams, callback) => { + let { StudyInstanceUIDs } = urlUtil.parse(queryParams, true); + + StudyInstanceUIDs = urlUtil.paramString.parseParam(StudyInstanceUIDs); + + debugger; + + if (!StudyInstanceUIDs) { + throw new Error( + 'Incomplete queryParams, missing StudyInstanceUIDs' + ); + } + + // const promises = StudyInstanceUIDs.map(StudyInstanceUID => + // retrieveStudyMetadata( + // wadoDicomWebClient, + // StudyInstanceUID, + // enableStudyLazyLoad + // ) + // ); + + // TEMP use dummy data. + const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary; + const instances = exampleInstances.map(naturalizeDataset); + + // TEMP + + dicomMetadataStore.addInstances(instances); + callback(instances); + }, + }, + }, }); } diff --git a/extensions/default/src/DicomWebDataSource/retrieveStudyMetadata.js b/extensions/default/src/DicomWebDataSource/retrieveStudyMetadata.js new file mode 100644 index 000000000..beb394a54 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/retrieveStudyMetadata.js @@ -0,0 +1,71 @@ +import RetrieveMetadata from './services/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} server Object with server configuration parameters + * @param {string} StudyInstanceUID The UID of the Study 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 + * @returns {Promise} that will be resolved with the metadata or rejected with the error + */ +export function retrieveStudyMetadata( + dicomWebClient, + StudyInstanceUID, + enableStudyLazyLoad, + filters +) { + // @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.` + ); + } + + // Already waiting on result? Return cached promise + if (StudyMetaDataPromises.has(StudyInstanceUID)) { + return StudyMetaDataPromises.get(StudyInstanceUID); + } + + // Create a promise to handle the data retrieval + const promise = new Promise((resolve, reject) => { + RetrieveMetadata( + dicomWebClient, + StudyInstanceUID, + enableStudyLazyLoad, + filters + ).then(function(data) { + resolve(data); + }, reject); + }); + + // Store the promise in cache + StudyMetaDataPromises.set(StudyInstanceUID, 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); + } +} diff --git a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadata.js b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadata.js new file mode 100644 index 000000000..f0474708e --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadata.js @@ -0,0 +1,35 @@ +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 {Object} dicomWebClient The dicomweb-client. + * @param {string} studyInstanceUid The Study Instance UID of the study which needs to be loaded + * @param {Object} [filters] - Object containing filters to be applied on retrieve metadata process + * @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against + * @returns {Object} A study descriptor object + */ +async function RetrieveMetadata( + dicomWebClient, + studyInstanceUid, + enableStudyLazyLoad, + filters = {} +) { + const RetrieveMetadataLoader = + enableStudyLazyLoad !== false + ? RetrieveMetadataLoaderAsync + : RetrieveMetadataLoaderSync; + + const retrieveMetadataLoader = new RetrieveMetadataLoader( + dicomWebClient, + studyInstanceUid, + filters + ); + const studyMetadata = retrieveMetadataLoader.execLoad(); + + return studyMetadata; +} + +export default RetrieveMetadata; diff --git a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoader.js b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoader.js new file mode 100644 index 000000000..b3222643a --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoader.js @@ -0,0 +1,59 @@ +/** + * 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} [filter.seriesInstanceUID] - series instance uid to filter results against + */ + constructor(client, studyInstanceUID, filters = {}) { + this.client = dicomwWebClient; + this.studyInstanceUID = studyInstanceUID; + this.filters = filters; + } + + 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) { + try { + result = await loader(); + if (result && result.length) { + break; // closes iterator in case data is retrieved successfully + } + } catch (e) { + throw e; + } + } + + 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) {} +} diff --git a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderAsync.js b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderAsync.js new file mode 100644 index 000000000..791d78b70 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderAsync.js @@ -0,0 +1,131 @@ +import RetrieveMetadataLoader from './retrieveMetadataLoader'; +import { sortStudySeries, sortingCriteria } from '../../sortStudy'; +import getSeriesInfo from '../../getSeriesInfo'; +import { + createStudyFromSOPInstanceList, + addInstancesToStudy, +} from './studyInstanceHelpers'; + +/** + * Map seriesList to an array of seriesInstanceUid + * @param {Arrays} seriesList list of Series Instance UIDs + * @returns {Arrays} A list of Series Instance UIDs + */ +function mapStudySeries(seriesList) { + return seriesList.map(series => getSeriesInfo(series).seriesInstanceUid); +} + +function attachSeriesLoader(server, study, seriesLoader) { + study.seriesLoader = Object.freeze({ + hasNext() { + return seriesLoader.hasNext(); + }, + async next() { + const series = await seriesLoader.next(); + await addInstancesToStudy(server, study, series.sopInstances); + return study.seriesMap[series.seriesInstanceUID]; + }, + }); +} + +/** + * 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; + }, + async next() { + const seriesInstanceUID = seriesInstanceUIDList.shift(); + const sopInstances = await client.retrieveSeriesMetadata({ + studyInstanceUID, + seriesInstanceUID, + }); + return { studyInstanceUID, seriesInstanceUID, sopInstances }; + }, + }); +} + +/** + * 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; + + if (seriesInstanceUID) { + const options = { + studyInstanceUID, + queryParams: { SeriesInstanceUID: seriesInstanceUID }, + }; + preLoaders.push(client.searchForSeries.bind(client, options)); + } + // Fallback preloader + preLoaders.push(client.searchForSeries.bind(client, { studyInstanceUID })); + + yield* preLoaders; + } + + async preLoad() { + const preLoaders = this.getPreLoaders(); + const result = await this.runLoaders(preLoaders); + + const seriesSorted = sortStudySeries( + result, + sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria + ); + const seriesInstanceUidsMap = mapStudySeries(seriesSorted); + + return seriesInstanceUidsMap; + } + + async load(preLoadData) { + const { client, studyInstanceUID } = this; + + const seriesAsyncLoader = makeSeriesAsyncLoader( + client, + studyInstanceUID, + preLoadData + ); + + const firstSeries = await seriesAsyncLoader.next(); + + return { + sopInstances: firstSeries.sopInstances, + asyncLoader: seriesAsyncLoader, + }; + } + + async posLoad(loadData) { + const { client } = this; + const { sopInstances, asyncLoader } = loadData; + + const study = await createStudyFromSOPInstanceList(server, sopInstances); + + if (asyncLoader.hasNext()) { + attachSeriesLoader(server, study, asyncLoader); + } + + return study; + } +} diff --git a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderSync.js b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderSync.js new file mode 100644 index 000000000..fe8a1adc6 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderSync.js @@ -0,0 +1,66 @@ +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) { + const { server } = this; + return createStudyFromSOPInstanceList(server, loadData); + } +} diff --git a/extensions/default/src/DicomWebDataSource/wado/studyInstanceHelpers.js b/extensions/default/src/DicomWebDataSource/wado/studyInstanceHelpers.js new file mode 100644 index 000000000..f4928a12f --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/wado/studyInstanceHelpers.js @@ -0,0 +1,483 @@ +import { api } from 'dicomweb-client'; +import DICOMWeb from '../../../DICOMWeb'; + +const WADOProxy = { + convertURL: (url, server) => { + // TODO: Remove all WADOProxy stuff from this file + return url; + }, +}; +function parseFloatArray(obj) { + const result = []; + + if (!obj) { + return result; + } + + const objs = obj.split('\\'); + for (let i = 0; i < objs.length; i++) { + result.push(parseFloat(objs[i])); + } + + return result; +} + +/** + * Simple cache schema for retrieved color palettes. + */ +const paletteColorCache = { + count: 0, + maxAge: 24 * 60 * 60 * 1000, // 24h cache? + entries: {}, + isValidUID: function(paletteUID) { + return typeof paletteUID === 'string' && paletteUID.length > 0; + }, + get: function(paletteUID) { + let entry = null; + if (this.entries.hasOwnProperty(paletteUID)) { + entry = this.entries[paletteUID]; + // check how the entry is... + if (Date.now() - entry.time > this.maxAge) { + // entry is too old... remove entry. + delete this.entries[paletteUID]; + this.count--; + entry = null; + } + } + return entry; + }, + add: function(entry) { + if (this.isValidUID(entry.uid)) { + let paletteUID = entry.uid; + if (this.entries.hasOwnProperty(paletteUID) !== true) { + this.count++; // increment cache entry count... + } + entry.time = Date.now(); + this.entries[paletteUID] = entry; + // @TODO: Add logic to get rid of old entries and reduce memory usage... + } + }, +}; + +/** + * Create a plain JS object that describes a study (a study descriptor object) + * @param {Object} server Object with server configuration parameters + * @param {Object} aSopInstance a SOP Instance from which study information will be added + */ +function createStudy(server, aSopInstance) { + // TODO: Pass a reference ID to the server instead of including the URLs here + return { + seriesList: [], + seriesMap: Object.create(null), + seriesLoader: null, + wadoUriRoot: server.wadoUriRoot, + wadoRoot: server.wadoRoot, + qidoRoot: server.qidoRoot, + patientName: DICOMWeb.getName(aSopInstance['00100010']), + patientId: DICOMWeb.getString(aSopInstance['00100020']), + patientAge: DICOMWeb.getNumber(aSopInstance['00101010']), + patientSize: DICOMWeb.getNumber(aSopInstance['00101020']), + patientWeight: DICOMWeb.getNumber(aSopInstance['00101030']), + accessionNumber: DICOMWeb.getString(aSopInstance['00080050']), + studyDate: DICOMWeb.getString(aSopInstance['00080020']), + modalities: DICOMWeb.getString(aSopInstance['00080061']), + studyDescription: DICOMWeb.getString(aSopInstance['00081030']), + imageCount: DICOMWeb.getString(aSopInstance['00201208']), + studyInstanceUid: DICOMWeb.getString(aSopInstance['0020000D']), + institutionName: DICOMWeb.getString(aSopInstance['00080080']), + }; +} + +/** Returns a WADO url for an instance + * + * @param studyInstanceUid + * @param seriesInstanceUid + * @param sopInstanceUid + * @returns {string} + */ +function buildInstanceWadoUrl( + server, + studyInstanceUid, + seriesInstanceUid, + sopInstanceUid +) { + // TODO: This can be removed, since DICOMWebClient has the same function. Not urgent, though + 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 `${server.wadoUriRoot}?${paramString}`; +} + +function buildInstanceWadoRsUri( + server, + studyInstanceUid, + seriesInstanceUid, + sopInstanceUid +) { + return `${server.wadoRoot}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`; +} + +function buildInstanceFrameWadoRsUri( + server, + studyInstanceUid, + seriesInstanceUid, + sopInstanceUid, + frame +) { + const baseWadoRsUri = buildInstanceWadoRsUri( + server, + studyInstanceUid, + seriesInstanceUid, + sopInstanceUid + ); + frame = frame != null || 1; + + return `${baseWadoRsUri}/frames/${frame}`; +} + +function getFrameIncrementPointer(element) { + const frameIncrementPointerNames = { + '00181065': 'frameTimeVector', + '00181063': 'frameTime', + }; + + if (!element || !element.Value || !element.Value.length) { + return; + } + + const value = element.Value[0]; + return frameIncrementPointerNames[value]; +} + +function getRadiopharmaceuticalInfo(instance) { + const modality = DICOMWeb.getString(instance['00080060']); + + if (modality !== 'PT') { + return; + } + + const radiopharmaceuticalInfo = instance['00540016']; + if ( + radiopharmaceuticalInfo === undefined || + !radiopharmaceuticalInfo.Value || + !radiopharmaceuticalInfo.Value.length + ) { + return; + } + + const firstPetRadiopharmaceuticalInfo = radiopharmaceuticalInfo.Value[0]; + return { + radiopharmaceuticalStartTime: DICOMWeb.getString( + firstPetRadiopharmaceuticalInfo['00181072'] + ), + radionuclideTotalDose: DICOMWeb.getNumber( + firstPetRadiopharmaceuticalInfo['00181074'] + ), + radionuclideHalfLife: DICOMWeb.getNumber( + firstPetRadiopharmaceuticalInfo['00181075'] + ), + }; +} + +/** + * Parses the SourceImageSequence, if it exists, in order + * to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID + * is used to refer to this image in any accompanying DICOM-SR documents. + * + * @param instance + * @returns {String} The ReferenceSOPInstanceUID + */ +function getSourceImageInstanceUid(instance) { + // TODO= Parse the whole Source Image Sequence + // This is a really poor workaround for now. + // Later we should probably parse the whole sequence. + var SourceImageSequence = instance['00082112']; + if ( + SourceImageSequence && + SourceImageSequence.Value && + SourceImageSequence.Value.length && + SourceImageSequence.Value[0]['00081155'].Value + ) { + return SourceImageSequence.Value[0]['00081155'].Value[0]; + } +} + +async function makeSOPInstance(server, study, instance) { + const { studyInstanceUid } = study; + const seriesInstanceUid = DICOMWeb.getString(instance['0020000E']); + let series = study.seriesMap[seriesInstanceUid]; + + if (!series) { + series = { + seriesInstanceUid, + seriesDescription: DICOMWeb.getString(instance['0008103E']), + modality: DICOMWeb.getString(instance['00080060']), + seriesNumber: DICOMWeb.getNumber(instance['00200011']), + seriesDate: DICOMWeb.getString(instance['00080021']), + seriesTime: DICOMWeb.getString(instance['00080031']), + instances: [], + }; + study.seriesMap[seriesInstanceUid] = series; + study.seriesList.push(series); + } + + const sopInstanceUid = DICOMWeb.getString(instance['00080018']); + const wadouri = buildInstanceWadoUrl( + server, + studyInstanceUid, + seriesInstanceUid, + sopInstanceUid + ); + const baseWadoRsUri = buildInstanceWadoRsUri( + server, + studyInstanceUid, + seriesInstanceUid, + sopInstanceUid + ); + const wadorsuri = buildInstanceFrameWadoRsUri( + server, + studyInstanceUid, + seriesInstanceUid, + sopInstanceUid + ); + + const sopInstance = { + imageType: DICOMWeb.getString(instance['00080008']), + sopClassUid: DICOMWeb.getString(instance['00080016']), + modality: DICOMWeb.getString(instance['00080060']), + sopInstanceUid, + instanceNumber: DICOMWeb.getNumber(instance['00200013']), + imagePositionPatient: DICOMWeb.getString(instance['00200032']), + imageOrientationPatient: DICOMWeb.getString(instance['00200037']), + frameOfReferenceUID: DICOMWeb.getString(instance['00200052']), + sliceLocation: DICOMWeb.getNumber(instance['00201041']), + samplesPerPixel: DICOMWeb.getNumber(instance['00280002']), + photometricInterpretation: DICOMWeb.getString(instance['00280004']), + planarConfiguration: DICOMWeb.getNumber(instance['00280006']), + rows: DICOMWeb.getNumber(instance['00280010']), + columns: DICOMWeb.getNumber(instance['00280011']), + pixelSpacing: DICOMWeb.getString(instance['00280030']), + pixelAspectRatio: DICOMWeb.getString(instance['00280034']), + bitsAllocated: DICOMWeb.getNumber(instance['00280100']), + bitsStored: DICOMWeb.getNumber(instance['00280101']), + highBit: DICOMWeb.getNumber(instance['00280102']), + pixelRepresentation: DICOMWeb.getNumber(instance['00280103']), + smallestPixelValue: DICOMWeb.getNumber(instance['00280106']), + largestPixelValue: DICOMWeb.getNumber(instance['00280107']), + windowCenter: DICOMWeb.getString(instance['00281050']), + windowWidth: DICOMWeb.getString(instance['00281051']), + rescaleIntercept: DICOMWeb.getNumber(instance['00281052']), + rescaleSlope: DICOMWeb.getNumber(instance['00281053']), + rescaleType: DICOMWeb.getNumber(instance['00281054']), + sourceImageInstanceUid: getSourceImageInstanceUid(instance), + laterality: DICOMWeb.getString(instance['00200062']), + viewPosition: DICOMWeb.getString(instance['00185101']), + acquisitionDateTime: DICOMWeb.getString(instance['0008002A']), + numberOfFrames: DICOMWeb.getNumber(instance['00280008']), + frameIncrementPointer: getFrameIncrementPointer(instance['00280009']), + frameTime: DICOMWeb.getNumber(instance['00181063']), + frameTimeVector: parseFloatArray(DICOMWeb.getString(instance['00181065'])), + sliceThickness: DICOMWeb.getNumber(instance['00180050']), + spacingBetweenSlices: DICOMWeb.getString(instance['00180088']), + lossyImageCompression: DICOMWeb.getString(instance['00282110']), + derivationDescription: DICOMWeb.getString(instance['00282111']), + lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']), + lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']), + echoNumber: DICOMWeb.getString(instance['00180086']), + contrastBolusAgent: DICOMWeb.getString(instance['00180010']), + radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance), + baseWadoRsUri: baseWadoRsUri, + wadouri: WADOProxy.convertURL(wadouri, server), + wadorsuri: WADOProxy.convertURL(wadorsuri, server), + wadoRoot: server.wadoRoot, + imageRendering: server.imageRendering, + thumbnailRendering: server.thumbnailRendering, + }; + + // Get additional information if the instance uses "PALETTE COLOR" photometric interpretation + if (sopInstance.photometricInterpretation === 'PALETTE COLOR') { + const redPaletteColorLookupTableDescriptor = parseFloatArray( + DICOMWeb.getString(instance['00281101']) + ); + const greenPaletteColorLookupTableDescriptor = parseFloatArray( + DICOMWeb.getString(instance['00281102']) + ); + const bluePaletteColorLookupTableDescriptor = parseFloatArray( + DICOMWeb.getString(instance['00281103']) + ); + const palettes = await getPaletteColors( + server, + instance, + redPaletteColorLookupTableDescriptor + ); + + if (palettes) { + if (palettes.uid) { + sopInstance.paletteColorLookupTableUID = palettes.uid; + } + + sopInstance.redPaletteColorLookupTableData = palettes.red; + sopInstance.greenPaletteColorLookupTableData = palettes.green; + sopInstance.bluePaletteColorLookupTableData = palettes.blue; + sopInstance.redPaletteColorLookupTableDescriptor = redPaletteColorLookupTableDescriptor; + sopInstance.greenPaletteColorLookupTableDescriptor = greenPaletteColorLookupTableDescriptor; + sopInstance.bluePaletteColorLookupTableDescriptor = bluePaletteColorLookupTableDescriptor; + } + } + + series.instances.push(sopInstance); + return sopInstance; +} + +/** + * Convert String to ArrayBuffer + * + * @param {String} str Input String + * @return {ArrayBuffer} Output converted ArrayBuffer + */ +function str2ab(str) { + const strLen = str.length; + const bytes = new Uint8Array(strLen); + + for (let i = 0; i < strLen; i++) { + bytes[i] = str.charCodeAt(i); + } + + return bytes.buffer; +} + +function getPaletteColor(server, instance, tag, lutDescriptor) { + const numLutEntries = lutDescriptor[0]; + const bits = lutDescriptor[2]; + + const readUInt16 = (byteArray, position) => { + return byteArray[position] + byteArray[position + 1] * 256; + }; + + const arrayBufferToPaletteColorLUT = arraybuffer => { + const byteArray = new Uint8Array(arraybuffer); + const lut = []; + + if (bits === 16) { + for (let i = 0; i < numLutEntries; i++) { + lut[i] = readUInt16(byteArray, i * 2); + } + } else { + for (let i = 0; i < numLutEntries; i++) { + lut[i] = byteArray[i]; + } + } + + return lut; + }; + + if (instance[tag].BulkDataURI) { + let uri = WADOProxy.convertURL(instance[tag].BulkDataURI, server); + + // TODO: Workaround for dcm4chee behind SSL-terminating proxy returning + // incorrect bulk data URIs + if (server.wadoRoot.indexOf('https') === 0 && !uri.includes('https')) { + uri = uri.replace('http', 'https'); + } + + const config = { + url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used + headers: DICOMWeb.getAuthorizationHeader(server), + }; + const dicomWeb = new api.DICOMwebClient(config); + const options = { + BulkDataURI: uri, + }; + + return dicomWeb + .retrieveBulkData(options) + .then(result => result[0]) + .then(arrayBufferToPaletteColorLUT); + } else if (instance[tag].InlineBinary) { + const inlineBinaryData = atob(instance[tag].InlineBinary); + const arraybuf = str2ab(inlineBinaryData); + + return arrayBufferToPaletteColorLUT(arraybuf); + } + + throw new Error( + 'Palette Color LUT was not provided as InlineBinary or BulkDataURI' + ); +} + +/** + * Fetch palette colors for instances with "PALETTE COLOR" photometricInterpretation. + * + * @param server {Object} Current server; + * @param instance {Object} The retrieved instance metadata; + * @returns {String} The ReferenceSOPInstanceUID + */ +async function getPaletteColors(server, instance, lutDescriptor) { + let paletteUID = DICOMWeb.getString(instance['00281199']); + + return new Promise((resolve, reject) => { + let entry; + if (paletteColorCache.isValidUID(paletteUID)) { + entry = paletteColorCache.get(paletteUID); + + if (entry) { + return resolve(entry); + } + } + + // no entry in cache... Fetch remote data. + const r = getPaletteColor(server, instance, '00281201', lutDescriptor); + const g = getPaletteColor(server, instance, '00281202', lutDescriptor); + const b = getPaletteColor(server, instance, '00281203', lutDescriptor); + + const promises = [r, g, b]; + + Promise.all(promises).then(args => { + entry = { + red: args[0], + green: args[1], + blue: args[2], + }; + + // when paletteUID is present, the entry can be cached... + entry.uid = paletteUID; + paletteColorCache.add(entry); + + resolve(entry); + }); + }); +} + +/** + * Add a list of SOP Instances to a given study object descriptor + * @param {Object} server Object with server configuration parameters + * @param {Object} study The study descriptor to which the given SOP instances will be added + * @param {Array} sopInstanceList A list of SOP instance objects + */ +async function addInstancesToStudy(server, study, sopInstanceList) { + return Promise.all( + sopInstanceList.map(function(sopInstance) { + return makeSOPInstance(server, study, sopInstance); + }) + ); +} + +const createStudyFromSOPInstanceList = async (server, sopInstanceList) => { + if (Array.isArray(sopInstanceList) && sopInstanceList.length > 0) { + const firstSopInstance = sopInstanceList[0]; + const study = createStudy(server, firstSopInstance); + await addInstancesToStudy(server, study, sopInstanceList); + return study; + } + throw new Error('Failed to create study out of provided SOP instance list'); +}; + +export { createStudyFromSOPInstanceList, addInstancesToStudy }; diff --git a/extensions/default/src/getLayoutTemplateModule.js b/extensions/default/src/getLayoutTemplateModule.js index d07972e64..1cee775b6 100644 --- a/extensions/default/src/getLayoutTemplateModule.js +++ b/extensions/default/src/getLayoutTemplateModule.js @@ -170,7 +170,12 @@ const ViewportToolbar = () => { return ; }; -function viewerLayout({ leftPanels, rightPanels, extensionManager, displaySetInstanceUids }) { +function viewerLayout({ + leftPanels, + rightPanels, + extensionManager, + displaySetInstanceUids, +}) { const getPanelData = id => { const entry = extensionManager.getModuleEntry(id); // TODO, not sure why sidepanel content has to be JSX, and not a children prop? @@ -185,6 +190,8 @@ function viewerLayout({ leftPanels, rightPanels, extensionManager, displaySetIns }; }; + debugger; + const leftPanelComponents = leftPanels.map(getPanelData); const rightPanelComponents = rightPanels.map(getPanelData); diff --git a/extensions/default/src/getSopClassHandlerModule.js b/extensions/default/src/getSopClassHandlerModule.js index c42b43c61..bc70b1942 100644 --- a/extensions/default/src/getSopClassHandlerModule.js +++ b/extensions/default/src/getSopClassHandlerModule.js @@ -1,27 +1,31 @@ import { isImage } from '@ohif/core/src/utils/isImage'; import ImageSet from '@ohif/core/src/classes/ImageSet'; import isDisplaySetReconstructable from '@ohif/core/src/utils/isDisplaySetReconstructable'; +import id from './id'; + +const sopClassHandlerName = 'stack'; const isMultiFrame = instance => { return instance.NumberOfFrames > 1; }; -const makeDisplaySet = (instances) => { +const makeDisplaySet = instances => { const instance = instances[0]; const imageSet = new ImageSet(instances); // set appropriate attributes to image set... imageSet.setAttributes({ displaySetInstanceUid: imageSet.uid, // create a local alias for the imageSet UID - seriesDate: instance.SeriesDate, - seriesTime: instance.SeriesTime, - seriesInstanceUid: instance.SeriesInstanceUID, - seriesNumber: instance.SeriesNumber, - seriesDescription: instance.SeriesDescription, - numImageFrames: instances.length, - frameRate: instance.FrameTime, - modality: instance.Modality, + SeriesDate: instance.SeriesDate, + SeriesTime: instance.SeriesTime, + SeriesInstanceUid: instance.SeriesInstanceUID, + SeriesNumber: instance.SeriesNumber, + SeriesDescription: instance.SeriesDescription, + FrameRate: instance.FrameTime, + Modality: instance.Modality, isMultiFrame: isMultiFrame(instance), + mumImageFrames: instances.length, + SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`, }); // Sort the images in this series if needed @@ -30,8 +34,7 @@ const makeDisplaySet = (instances) => { imageSet.sortBy((a, b) => { // Sort by InstanceNumber (0020,0013) return ( - (parseInt(a.InstanceNumber) || 0) - - (parseInt(b.InstanceNumber) || 0) + (parseInt(a.InstanceNumber) || 0) - (parseInt(b.InstanceNumber) || 0) ); }); } @@ -78,7 +81,7 @@ function getSopClassUids(instances) { * @param {SeriesMetadata} series The series metadata object from which the display sets will be created * @returns {Array} The list of display sets created for the given series object */ -function getDisplaySetFromSeries(instances) { +function getDisplaySetsFromSeries(instances) { // If the series has no instances, stop here if (!instances || !instances.length) { throw new Error('No instances were provided'); @@ -325,9 +328,9 @@ const sopClassUids = [ function getSopClassHandlerModule() { return [ { - name: 'stack', + name: sopClassHandlerName, sopClassUids, - getDisplaySetFromSeries, + getDisplaySetsFromSeries, }, ]; } diff --git a/extensions/default/src/id.js b/extensions/default/src/id.js new file mode 100644 index 000000000..d98b866c8 --- /dev/null +++ b/extensions/default/src/id.js @@ -0,0 +1,3 @@ +const id = 'org.ohif.default'; + +export default id; diff --git a/extensions/default/src/index.js b/extensions/default/src/index.js index 3fae68f77..94267402f 100644 --- a/extensions/default/src/index.js +++ b/extensions/default/src/index.js @@ -3,15 +3,16 @@ import getDataSourcesModule from './getDataSourcesModule.js'; import getLayoutTemplateModule from './getLayoutTemplateModule.js'; import getPanelModule from './getPanelModule.js'; import getSopClassHandlerModule from './getSopClassHandlerModule.js'; +import id from './id'; export default { /** * Only required property. Should be a unique value across all extensions. */ - id: 'org.ohif.default', + id, getContextModule, getDataSourcesModule, getLayoutTemplateModule, getPanelModule, - getSopClassHandlerModule + getSopClassHandlerModule, }; diff --git a/modes.zip b/modes.zip new file mode 100644 index 000000000..1c33a2902 Binary files /dev/null and b/modes.zip differ diff --git a/platform/core/src/DataSources/DicomWebApi.js b/platform/core/src/DataSources/DicomWebApi.js deleted file mode 100644 index fad4c5102..000000000 --- a/platform/core/src/DataSources/DicomWebApi.js +++ /dev/null @@ -1,54 +0,0 @@ -import { api } from 'dicomweb-client'; -import { mapParams, search, processResults } from './qido.js'; -import IWebApiDataSource from './IWebApiDataSource.js'; - -/** - * - * @param {string} name - Data source name - * @param {string} wadoUriRoot - Legacy? - * @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} lazyLoadStudy - "enableStudyLazyLoad"; Request series meta async instead of blocking - */ -function createDicomWebApi(dicomWebConfig) { - const { qidoRoot } = dicomWebConfig; - const config = { - url: qidoRoot, - // headers: DICOMWeb.getAuthorizationHeader(server), - }; - - const dicomWebClient = new api.DICOMwebClient(config); - - return IWebApiDataSource.create({ - query: { - studies: { - mapParams: mapParams.bind(), - search: search.bind(undefined, dicomWebClient, null, null), - processResults: processResults.bind(), - }, - instances: { - search: (studyInstanceUid, queryParamaters) => - search.call( - undefined, - dicomWebClient, - studyInstanceUid, - null, - queryParamaters - ), - }, - }, - retrieveMetadata: (dicomMetadataStore, queryParamaters) => { - // TODO -> Write implementation - // WADO-RS retrieve study metadata. - // Then different WADO-RS retrieve series metadata calls. - // Then dicomMetadataStore.addInstance(instance) - // dicomMetadataStore naturalizes the instance and adds it to the store. - // dicomMetadataStore emits an even that the SopClassHandlerManager can listen to. - }, - }); -} - -export { createDicomWebApi }; diff --git a/platform/core/src/DataSources/DicomWebApi/qido.js b/platform/core/src/DataSources/DicomWebApi/qido.js deleted file mode 100644 index 0c5726615..000000000 --- a/platform/core/src/DataSources/DicomWebApi/qido.js +++ /dev/null @@ -1,178 +0,0 @@ -/** - * 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 { getString, getName, getModalities } from '../../../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']), - studyDate: getString(qidoStudy['00080020']), - studyTime: getString(qidoStudy['00080030']), - accessionNumber: getString(qidoStudy['00080050']), - referringPhysicianName: getString(qidoStudy['00080090']), - patientName: getName(qidoStudy['00100010']), - patientId: getString(qidoStudy['00100020']), - patientBirthdate: getString(qidoStudy['00100030']), - patientSex: getString(qidoStudy['00100040']), - studyId: getString(qidoStudy['00200010']), - numberOfStudyRelatedSeries: getString(qidoStudy['00201206']), - numberOfStudyRelatedInstances: getString(qidoStudy['00201208']), - studyDescription: getString(qidoStudy['00081030']), - modalities: getString( - getModalities(qidoStudy['00080060'], qidoStudy['00080061']) - ), - }) - ); - - return studies; -} - -/** - * - * @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} - Promise that resolves results - */ -function search( - dicomWebClient, - studyInstanceUid, - seriesInstanceUid, - queryParamaters -) { - const requestFn = studyInstanceUid - ? dicomWebClient.searchForInstances - : dicomWebClient.searchForStudies; - - // TODO: Current version does not apply query Params for `searchForInstances` call? - // Just sets `studyInstanceUid` in options... - return requestFn({ - studyInstanceUid, - queryParams: queryParamaters, - }); -} - -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) { - const commaSeparatedFields = [ - '00081030', // Study Description - '00080060', // Modality - // Add more fields here if you want them in the result - ].join(','); - - const parameters = { - PatientName: filter.patientName, - PatientID: filter.patientId, - AccessionNumber: filter.accessionNumber, - StudyDescription: filter.studyDescription, - ModalitiesInStudy: filter.modalitiesInStudy, - limit: filter.limit, - offset: filter.offset, - fuzzymatching: filter.fuzzymatching, - includefield: serverSupportsQIDOIncludeField ? commaSeparatedFields : 'all', - }; - - // build the StudyDate range parameter - if (filter.studyDateFrom || filter.studyDateTo) { - const dateFrom = _dateToString(new Date(filter.studyDateFrom)); - const dateTo = _dateToString(new Date(filter.studyDateTo)); - parameters.StudyDate = `${dateFrom}-${dateTo}`; - } - - // Build the StudyInstanceUID parameter - if (filter.studyInstanceUid) { - let studyUids = filter.studyInstanceUid; - studyUids = Array.isArray(studyUids) ? studyUids.join() : studyUids; - studyUids = studyUids.replace(/[^0-9.]+/g, '\\'); - parameters.StudyInstanceUID = studyUids; - } - - // Clean query params of undefined values. - const params = {}; - Object.keys(parameters).forEach(key => { - if (parameters[key] !== undefined && parameters[key] !== '') { - params[key] = parameters[key]; - } - }); - - return params; -} - -/** - * Creates a QIDO date string for a date range query - * Assumes the year is positive, at most 4 digits long. - * - * @param date The Date object to be formatted - * @returns {string} The formatted date string - */ -function _dateToString(date) { - if (!date) return ''; - let year = date.getFullYear().toString(); - let month = (date.getMonth() + 1).toString(); - let day = date.getDate().toString(); - year = '0'.repeat(4 - year.length).concat(year); - month = '0'.repeat(2 - month.length).concat(month); - day = '0'.repeat(2 - day.length).concat(day); - return ''.concat(year, month, day); -} - -export { mapParams, search, processResults }; diff --git a/platform/core/src/DataSources/IDataSource.js b/platform/core/src/DataSources/IDataSource.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/platform/core/src/DataSources/IWebApiDataSource.js b/platform/core/src/DataSources/IWebApiDataSource.js index 3848e6574..580d4de04 100644 --- a/platform/core/src/DataSources/IWebApiDataSource.js +++ b/platform/core/src/DataSources/IWebApiDataSource.js @@ -1,3 +1,5 @@ +import { dicomMetadataStore } from '@ohif/core'; + /** * Factory function that creates a new "Web API" data source. * A "Web API" data source is any source that fetches data over @@ -8,7 +10,7 @@ * It's worth noting that a single implementation of this interface * can define different underlying sources for "read" and "write" operations. */ -function create({ query, retrieve, retrieveMetadata }) { +function create({ query, retrieve }) { const defaultQuery = { studies: { /** @@ -31,12 +33,13 @@ function create({ query, retrieve, retrieveMetadata }) { instances: {}, }; - const defaultRetrieve = {}; + const defaultRetrieve = { + series: {}, + }; return { query, - retrieve: {}, - // retrieveMetadata: retrieveMetadata.bind(null, dicomMetadataStore); + retrieve, // then go get all series level metadata. // Store this in the DICOM MetadataStore. }; diff --git a/platform/core/src/dicomMetadataStore.js b/platform/core/src/dicomMetadataStore.js index b0bbe7038..5084ffcea 100644 --- a/platform/core/src/dicomMetadataStore.js +++ b/platform/core/src/dicomMetadataStore.js @@ -1,20 +1,104 @@ -import { createStudyFromSOPInstanceList, - addInstancesToStudy } from './studies/services/wado/studyInstanceHelpers'; -import * as dcmjs from 'dcmjs'; +import { + createStudyFromSOPInstanceList, + addInstancesToStudy, +} from './studies/services/wado/studyInstanceHelpers'; -const exampleInstances = [{"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"}}]; +import displaySetManager from './displaySetManager'; -const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary; -const instances = exampleInstances.map(naturalizeDataset); +class StudyMetadata { + constructor(StudyInstanceUID) { + this.StudyInstanceUID = StudyInstanceUID; + this.series = []; + } + addSeries(instances) { + this.series.push(new SeriesMetadata(instances)); + } +} + +class SeriesMetadata { + constructor(instances) { + const { SeriesInstanceUID } = instances[0]; + this.SeriesInstanceUID = SeriesInstanceUID; + this.instances = instances; + } +} + +const _model = { + studies: [], + // studies: [{ + // seriesLists: [ + // { + // // Series in study from dicom web server 1 (or different backend 1) + // series: [{ + // instances: [{ + // ...instanceMetadata // Naturalized DICOM. + // }], + // ...seriesMetadata + // }], + // clientName + // }, + // { + // // Series in study from dicom web server 2 (or different backend 2) + // }, + // ], + // ...studyMetadata, + // }] +}; + +function _getStudy(StudyInstanceUID) { + return _model.studies.find(StudyInstanceUID); +} + +function _getSeries(StudyInstanceUID, SeriesInstanceUID) { + const study = _getStudy(StudyInstanceUID); + + if (!study) { + return; + } + + return study.series.find( + aSeries => aSeries.SeriesInstanceUID === SeriesInstanceUID + ); +} + +function _getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) { + const series = _getSeries(StudyInstanceUID, SeriesInstanceUID); + + if (!series) { + return; + } + + return series.instances.find( + instance => instance.SOPInstanceUID === SOPInstanceUID + ); +} const dicomMetadataStore = { - onSeriesMetadataLoaded: (callback) => { - setTimeout(function() { - callback(instances); - }, 3000) - } + addInstances(instances) { + const { StudyInstanceUID } = instances[0]; + + let study = _model.studies.find( + study => study.StudyInstanceUID === StudyInstanceUID + ); + + if (!study) { + _model.studies.push(new StudyMetadata(StudyInstanceUID)); + + study = _model.studies[_model.studies.length - 1]; + } + + study.addSeries(instances); + }, + getStudy: _getStudy, + getSeries: _getSeries, + getInstance: _getInstance, }; +// + +// TODO => Add instances +//_addInstance(input) // arraybuffer, or other stuff + export { dicomMetadataStore }; export default dicomMetadataStore; diff --git a/platform/core/src/displaySetManager.js b/platform/core/src/displaySetManager.js new file mode 100644 index 000000000..210eb8356 --- /dev/null +++ b/platform/core/src/displaySetManager.js @@ -0,0 +1,92 @@ +class DisplaySetManager { + constructor() { + this.displaySets = []; + } + + init(extensionManager, SOPClassHandlerIds, viewModelContext) { + this.extensionManager = extensionManager; + this.SOPClassHandlerIds = SOPClassHandlerIds; + + const { + displaySetInstanceUids, + setDisplaySetInstanceUids, + } = viewModelContext; + + this.displaySetInstanceUids = displaySetInstanceUids; + this.setDisplaySetInstanceUids = setDisplaySetInstanceUids; + + // Reset displaySetInstanceUids + this.setDisplaySetInstanceUids([]); + } + + _addDisplaySets(displaySets) { + debugger; + //const displayInstanceUids = [...this.displaySetInstanceUids]; + + const addedDisplaySetUids = []; + + displaySets.forEach(displaySet => { + this.displaySets.push(displaySet); + + addedDisplaySetUids.push(displaySet.displaySetInstanceUid); + }); + + return addedDisplaySetUids; + } + + getDisplaySetsForSeries(SeriesInstanceUID) { + return this.displaySets.filter( + displaySet => displaySet.SeriesInstanceUID === SeriesInstanceUID + ); + } + + makeDisplaySets = instances => { + if (!instances || !instances.length) { + throw new Error('No instances were provided.'); + } + + const instance = instances[0]; + + debugger; + + const existingDisplaySets = + this.getDisplaySetsForSeries(instance.SeriesInstanceUID) || []; + + const SOPClassHandlerIds = this.SOPClassHandlerIds; + + for (let i = 0; i < SOPClassHandlerIds.length; i++) { + const SOPClassHandlerId = SOPClassHandlerIds[i]; + const handler = this.extensionManager.getModuleEntry(SOPClassHandlerId); + + if (handler.sopClassUids.includes(instance.SOPClassUID)) { + let addedDisplaySetUids; + + // Check if displaySets are already created using this SeriesInstanceUID/SOPClassHandler pair. + const cachedDisplaySets = existingDisplaySets.filter( + displaySet => displaySet.SOPClassHandlerId === SOPClassHandlerId + ); + + if (cachedDisplaySets.length) { + addedDisplaySetUids = cachedDisplaySets.map( + displaySet => displaySet.displaySetInstanceUid + ); + } else { + const displaySets = handler.getDisplaySetsFromSeries(instances); + + addedDisplaySetUids = this._addDisplaySets(displaySets); + } + + debugger; + + setDisplaySetInstanceUids([ + ...addedDisplaySetUids, + ...this.displaySetInstanceUids, + ]); + } + } + }; +} + +const displaySetManager = new DisplaySetManager(); + +export default displaySetManager; diff --git a/platform/core/src/extensions/ExtensionManager.js b/platform/core/src/extensions/ExtensionManager.js index 167a88769..e5bbe616b 100644 --- a/platform/core/src/extensions/ExtensionManager.js +++ b/platform/core/src/extensions/ExtensionManager.js @@ -25,15 +25,15 @@ export default class ExtensionManager { * * @param {Object[]} extensions - Array of extensions */ - registerExtensions = extensions => { + registerExtensions = (extensions, dataSources) => { extensions.forEach(extension => { const hasConfiguration = Array.isArray(extension); if (hasConfiguration) { const [ohifExtension, configuration] = extension; - this.registerExtension(ohifExtension, configuration); + this.registerExtension(ohifExtension, dataSources, configuration); } else { - this.registerExtension(extension); + this.registerExtension(extension, dataSources); } }); }; @@ -44,7 +44,7 @@ export default class ExtensionManager { * @param {Object} extension * @param {Object} configuration */ - registerExtension = (extension, configuration = {}) => { + registerExtension = (extension, dataSources, configuration = {}) => { if (!extension) { log.warn( 'Attempting to register a null/undefined extension. Exiting early.' @@ -89,7 +89,12 @@ export default class ExtensionManager { ); if (extensionModule) { - this._initSpecialModuleTypes(moduleType, extensionModule); + this._initSpecialModuleTypes( + extensionId, + moduleType, + extensionModule, + dataSources + ); this.modules[moduleType].push({ extensionId, @@ -112,9 +117,9 @@ export default class ExtensionManager { return this.modulesMap[stringEntry]; }; - getDataSource = dataSourceId => { + getDataSources = dataSourceName => { // Note: this currently uses the data source name, which feels weird... - return this.dataSourceMap[dataSourceId]; + return this.dataSourceMap[dataSourceName]; }; /** @@ -155,7 +160,12 @@ export default class ExtensionManager { } }; - _initSpecialModuleTypes = (moduleType, extensionModule) => { + _initSpecialModuleTypes = ( + extensionId, + moduleType, + extensionModule, + dataSources + ) => { switch (moduleType) { case 'commandsModule': { const { definitions, defaultContext } = extensionModule; @@ -168,7 +178,25 @@ export default class ExtensionManager { } case 'dataSourcesModule': { extensionModule.forEach(element => { - this.dataSourceMap[element.name] = element; + const namespace = `${extensionId}.${moduleType}.${element.name}`; + + dataSources.forEach(dataSource => { + if (dataSource.namespace === namespace) { + const dataSourceInstance = element.createDataSource( + dataSource.configuration + ); + + if (this.dataSourceMap[dataSource.sourceName]) { + this.dataSourceMap[dataSource.sourceName].push( + dataSourceInstance + ); + } else { + this.dataSourceMap[dataSource.sourceName] = [ + dataSourceInstance, + ]; + } + } + }); }); break; diff --git a/platform/core/src/index.js b/platform/core/src/index.js index bc845328e..6d4c691e1 100644 --- a/platform/core/src/index.js +++ b/platform/core/src/index.js @@ -19,6 +19,7 @@ import studies from './studies/'; import ui from './ui'; import user from './user.js'; import dicomMetadataStore from './dicomMetadataStore'; +import displaySetManager from './displaySetManager'; import utils, { hotkeys } from './utils/'; import { @@ -66,6 +67,7 @@ const OHIF = { MeasurementService, IWebApiDataSource, dicomMetadataStore, + displaySetManager, }; export { @@ -102,6 +104,7 @@ export { MeasurementService, IWebApiDataSource, dicomMetadataStore, + displaySetManager, }; export { OHIF }; diff --git a/platform/viewer/public/config/default.js b/platform/viewer/public/config/default.js index 1d31985b3..d4a3a6df6 100644 --- a/platform/viewer/public/config/default.js +++ b/platform/viewer/public/config/default.js @@ -7,8 +7,9 @@ window.config = { // filterQueryParam: false, dataSources: [ { - friendlyName: "Rodrigo's DICOMWeb Server", - namespace: 'org.ohif.default.datasources.dicomweb', + friendlyName: 'dcmjs DICOMWeb Server', + namespace: 'org.ohif.default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', configuration: { name: 'DCM4CHEE', wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado', diff --git a/platform/viewer/src/appInit.js b/platform/viewer/src/appInit.js index 279803a75..98d1b136c 100644 --- a/platform/viewer/src/appInit.js +++ b/platform/viewer/src/appInit.js @@ -50,10 +50,10 @@ function appInit(appConfigOrFunc, defaultExtensions) { * Example: [ext1, ext2, ext3] * Example2: [[ext1, config], ext2, [ext3, config]] */ - extensionManager.registerExtensions([ - ...defaultExtensions, - ...appConfig.extensions, - ]); + extensionManager.registerExtensions( + [...defaultExtensions, ...appConfig.extensions], + appConfig.dataSources + ); // TODO: Init global hotkeys, or the hotkeys manager? // TODO: We no longer use `utils.addServer` @@ -69,7 +69,8 @@ function appInit(appConfigOrFunc, defaultExtensions) { modes.push(window.exampleMode); } - const appRoutes = buildModeRoutes(modes, extensionManager) || []; + const appRoutes = + buildModeRoutes(modes, appConfig.dataSources, extensionManager) || []; return { appConfig, diff --git a/platform/viewer/src/routes/ModeRoute.js b/platform/viewer/src/routes/ModeRoute.js index eca400c70..c449e7287 100644 --- a/platform/viewer/src/routes/ModeRoute.js +++ b/platform/viewer/src/routes/ModeRoute.js @@ -1,20 +1,26 @@ import React, { useContext } from 'react'; -import SOPClassHandlerManager from './SOPClassHandlerManager'; +import { displaySetManager } from '@ohif/core'; import ViewModelContext from './ViewModelContext'; import Compose from './Compose'; export default function ModeRoute({ location, mode, - dataSourceId, + dataSourceName, extensionManager, }) { const { routes, sopClassHandlers, extensions } = mode; - const dataSource = extensionManager.getDataSource(dataSourceId); + const dataSources = extensionManager.getDataSources(dataSourceName); - const { displaySetInstanceUids, setDisplaySetInstanceUids } = useContext( - ViewModelContext - ); + // TODO: For now assume one unique datasource. + + const dataSource = dataSources[0]; + + const viewModelContext = useContext(ViewModelContext); + + console.log(viewModelContext); + + const { displaySetInstanceUids } = viewModelContext; // Deal with toolbar. // Only handling one route per mode for now @@ -28,28 +34,25 @@ export default function ModeRoute({ const LayoutComponent = layoutTemplateModuleEntry.component; // Add SOPClassHandlers to a new SOPClassManager. - const manager = new SOPClassHandlerManager( - extensionManager, - sopClassHandlers - ); - /*setInterval(() => { - setDisplaySetInstanceUids(manager.displaySets); - }, 5000);*/ + debugger; + + displaySetManager.init(extensionManager, sopClassHandlers, viewModelContext); + + // setInterval(() => { + // setDisplaySetInstanceUids(manager.displaySets); + // }, 5000); const queryParams = location.search; // Call the data source to start building the view model? - //dataSource(queryParams); - //metadataStore.onModified(); - - const onUpdatedCallback = () => { - // TODO: This should append, not create from scratch so we don't nuke existing display sets - // when e.g. a new series arrives - //manager.createDisplaySets.then(setDisplaySetInstanceUids); - }; + dataSource.retrieve.series.metadata( + queryParams, + displaySetManager.makeDisplaySets + ); // For each extension, look up their context modules + // TODO: move to extension manager. let contextModules = []; extensions.forEach(extensionId => { const allRegisteredModuleIds = Object.keys(extensionManager.modulesMap); diff --git a/platform/viewer/src/routes/SOPClassHandlerManager.js b/platform/viewer/src/routes/SOPClassHandlerManager.js deleted file mode 100644 index 815af99d9..000000000 --- a/platform/viewer/src/routes/SOPClassHandlerManager.js +++ /dev/null @@ -1,31 +0,0 @@ -import { dicomMetadataStore } from '@ohif/core'; - -export default class SOPClassHandlerManager { - constructor(extensionManager, sopClassHandlerIds) { - this.SOPClassHandlers = sopClassHandlerIds.map( - extensionManager.getModuleEntry - ); - - dicomMetadataStore.onSeriesMetadataLoaded(this.onSeriesMetadataLoaded); - - // TODO, this is unclear. How are we getting the created display sets out? - this.displaySets = []; - } - - onSeriesMetadataLoaded = instances => { - if (!instances || !instances.length) { - throw new Error("No instances were provided."); - } - - const SOPClassHandlers = this.SOPClassHandlers; - - for (let i = 0; i < SOPClassHandlers.length; i++) { - const handler = SOPClassHandlers[i]; - - if (handler.sopClassUids.includes(instances[0].SOPClassUID)) { - // TODO: This step is still unclear to me - this.displaySets = handler.getDisplaySetFromSeries(instances).map(a => a.displaySetInstanceUid); - } - } - }; -} diff --git a/platform/viewer/src/routes/buildModeRoutes.js b/platform/viewer/src/routes/buildModeRoutes.js index 9490a7b27..d7e285445 100644 --- a/platform/viewer/src/routes/buildModeRoutes.js +++ b/platform/viewer/src/routes/buildModeRoutes.js @@ -22,28 +22,35 @@ import { ViewModelProvider } from './ViewModelContext'; /:modeId/:modeRoute/?queryParameters=example */ -export default function buildModeRoutes(modes, extensionManager) { +export default function buildModeRoutes(modes, dataSources, extensionManager) { const routes = []; - // TODO: Build api for this. - // Currently builds an endpoint for all data sources, which probably - // doesn't make sense - const dataSources = Object.keys(extensionManager.dataSourceMap).map(a => - extensionManager.getDataSource(a) - ); + // const dataSources = Object.keys(extensionManager.dataSourceMap).map(a => + // extensionManager.getDataSources(a) + // ); + + const dataSourceNames = []; + + dataSources.forEach(dataSource => { + const { sourceName } = dataSource; + if (!dataSourceNames.includes(sourceName)) { + dataSourceNames.push(sourceName); + } + }); modes.forEach(mode => { - dataSources.forEach(dataSource => { + // todo: for each route. add route to path. + dataSourceNames.forEach(dataSourceName => { // TODO: name vs id - const dataSourceId = dataSource.name; - const path = `/${mode.id}/${dataSourceId}`; + const path = `/${mode.id}/${dataSourceName}`; + // TODO move up. const component = ({ location }) => (