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 }) => (