WIP build up displaysets.
This commit is contained in:
parent
d4f51c5e3c
commit
5c19671d3c
@ -31,7 +31,8 @@
|
|||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"webpack": "^4.0.0"
|
"webpack": "^4.0.0",
|
||||||
|
"dcmjs": "^0.12.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.7.6"
|
"@babel/runtime": "7.7.6"
|
||||||
|
|||||||
@ -1,6 +1,291 @@
|
|||||||
import { api } from 'dicomweb-client';
|
import { api } from 'dicomweb-client';
|
||||||
import { mapParams, search as qidoSearch, processResults } from './qido.js';
|
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
|
* @param {bool} lazyLoadStudy - "enableStudyLazyLoad"; Request series meta async instead of blocking
|
||||||
*/
|
*/
|
||||||
function createDicomWebApi(dicomWebConfig) {
|
function createDicomWebApi(dicomWebConfig) {
|
||||||
const { qidoRoot } = dicomWebConfig;
|
const { qidoRoot, wadoRoot, enableStudyLazyLoad } = dicomWebConfig;
|
||||||
const config = {
|
|
||||||
|
const qidoConfig = {
|
||||||
url: qidoRoot,
|
url: qidoRoot,
|
||||||
// headers: DICOMWeb.getAuthorizationHeader(server),
|
// 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({
|
return IWebApiDataSource.create({
|
||||||
query: {
|
query: {
|
||||||
@ -31,7 +324,7 @@ function createDicomWebApi(dicomWebConfig) {
|
|||||||
mapParams(origParams) || {};
|
mapParams(origParams) || {};
|
||||||
|
|
||||||
const results = await qidoSearch(
|
const results = await qidoSearch(
|
||||||
dicomWebClient,
|
qidoDicomWebClient,
|
||||||
studyInstanceUid,
|
studyInstanceUid,
|
||||||
seriesInstanceUid,
|
seriesInstanceUid,
|
||||||
mappedParams
|
mappedParams
|
||||||
@ -45,13 +338,47 @@ function createDicomWebApi(dicomWebConfig) {
|
|||||||
search: (studyInstanceUid, queryParamaters) =>
|
search: (studyInstanceUid, queryParamaters) =>
|
||||||
qidoSearch.call(
|
qidoSearch.call(
|
||||||
undefined,
|
undefined,
|
||||||
dicomWebClient,
|
qidoDicomWebClient,
|
||||||
studyInstanceUid,
|
studyInstanceUid,
|
||||||
null,
|
null,
|
||||||
queryParamaters
|
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);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
@ -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) {}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 };
|
||||||
@ -170,7 +170,12 @@ const ViewportToolbar = () => {
|
|||||||
return <Toolbar type="secondary" tools={tools} />;
|
return <Toolbar type="secondary" tools={tools} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
function viewerLayout({ leftPanels, rightPanels, extensionManager, displaySetInstanceUids }) {
|
function viewerLayout({
|
||||||
|
leftPanels,
|
||||||
|
rightPanels,
|
||||||
|
extensionManager,
|
||||||
|
displaySetInstanceUids,
|
||||||
|
}) {
|
||||||
const getPanelData = id => {
|
const getPanelData = id => {
|
||||||
const entry = extensionManager.getModuleEntry(id);
|
const entry = extensionManager.getModuleEntry(id);
|
||||||
// TODO, not sure why sidepanel content has to be JSX, and not a children prop?
|
// 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 leftPanelComponents = leftPanels.map(getPanelData);
|
||||||
const rightPanelComponents = rightPanels.map(getPanelData);
|
const rightPanelComponents = rightPanels.map(getPanelData);
|
||||||
|
|
||||||
|
|||||||
@ -1,27 +1,31 @@
|
|||||||
import { isImage } from '@ohif/core/src/utils/isImage';
|
import { isImage } from '@ohif/core/src/utils/isImage';
|
||||||
import ImageSet from '@ohif/core/src/classes/ImageSet';
|
import ImageSet from '@ohif/core/src/classes/ImageSet';
|
||||||
import isDisplaySetReconstructable from '@ohif/core/src/utils/isDisplaySetReconstructable';
|
import isDisplaySetReconstructable from '@ohif/core/src/utils/isDisplaySetReconstructable';
|
||||||
|
import id from './id';
|
||||||
|
|
||||||
|
const sopClassHandlerName = 'stack';
|
||||||
|
|
||||||
const isMultiFrame = instance => {
|
const isMultiFrame = instance => {
|
||||||
return instance.NumberOfFrames > 1;
|
return instance.NumberOfFrames > 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const makeDisplaySet = (instances) => {
|
const makeDisplaySet = instances => {
|
||||||
const instance = instances[0];
|
const instance = instances[0];
|
||||||
const imageSet = new ImageSet(instances);
|
const imageSet = new ImageSet(instances);
|
||||||
|
|
||||||
// set appropriate attributes to image set...
|
// set appropriate attributes to image set...
|
||||||
imageSet.setAttributes({
|
imageSet.setAttributes({
|
||||||
displaySetInstanceUid: imageSet.uid, // create a local alias for the imageSet UID
|
displaySetInstanceUid: imageSet.uid, // create a local alias for the imageSet UID
|
||||||
seriesDate: instance.SeriesDate,
|
SeriesDate: instance.SeriesDate,
|
||||||
seriesTime: instance.SeriesTime,
|
SeriesTime: instance.SeriesTime,
|
||||||
seriesInstanceUid: instance.SeriesInstanceUID,
|
SeriesInstanceUid: instance.SeriesInstanceUID,
|
||||||
seriesNumber: instance.SeriesNumber,
|
SeriesNumber: instance.SeriesNumber,
|
||||||
seriesDescription: instance.SeriesDescription,
|
SeriesDescription: instance.SeriesDescription,
|
||||||
numImageFrames: instances.length,
|
FrameRate: instance.FrameTime,
|
||||||
frameRate: instance.FrameTime,
|
Modality: instance.Modality,
|
||||||
modality: instance.Modality,
|
|
||||||
isMultiFrame: isMultiFrame(instance),
|
isMultiFrame: isMultiFrame(instance),
|
||||||
|
mumImageFrames: instances.length,
|
||||||
|
SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort the images in this series if needed
|
// Sort the images in this series if needed
|
||||||
@ -30,8 +34,7 @@ const makeDisplaySet = (instances) => {
|
|||||||
imageSet.sortBy((a, b) => {
|
imageSet.sortBy((a, b) => {
|
||||||
// Sort by InstanceNumber (0020,0013)
|
// Sort by InstanceNumber (0020,0013)
|
||||||
return (
|
return (
|
||||||
(parseInt(a.InstanceNumber) || 0) -
|
(parseInt(a.InstanceNumber) || 0) - (parseInt(b.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
|
* @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
|
* @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 the series has no instances, stop here
|
||||||
if (!instances || !instances.length) {
|
if (!instances || !instances.length) {
|
||||||
throw new Error('No instances were provided');
|
throw new Error('No instances were provided');
|
||||||
@ -325,9 +328,9 @@ const sopClassUids = [
|
|||||||
function getSopClassHandlerModule() {
|
function getSopClassHandlerModule() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'stack',
|
name: sopClassHandlerName,
|
||||||
sopClassUids,
|
sopClassUids,
|
||||||
getDisplaySetFromSeries,
|
getDisplaySetsFromSeries,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
3
extensions/default/src/id.js
Normal file
3
extensions/default/src/id.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
const id = 'org.ohif.default';
|
||||||
|
|
||||||
|
export default id;
|
||||||
@ -3,15 +3,16 @@ import getDataSourcesModule from './getDataSourcesModule.js';
|
|||||||
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
|
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
|
||||||
import getPanelModule from './getPanelModule.js';
|
import getPanelModule from './getPanelModule.js';
|
||||||
import getSopClassHandlerModule from './getSopClassHandlerModule.js';
|
import getSopClassHandlerModule from './getSopClassHandlerModule.js';
|
||||||
|
import id from './id';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
* Only required property. Should be a unique value across all extensions.
|
* Only required property. Should be a unique value across all extensions.
|
||||||
*/
|
*/
|
||||||
id: 'org.ohif.default',
|
id,
|
||||||
getContextModule,
|
getContextModule,
|
||||||
getDataSourcesModule,
|
getDataSourcesModule,
|
||||||
getLayoutTemplateModule,
|
getLayoutTemplateModule,
|
||||||
getPanelModule,
|
getPanelModule,
|
||||||
getSopClassHandlerModule
|
getSopClassHandlerModule,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 };
|
|
||||||
@ -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<results>} - 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 };
|
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { dicomMetadataStore } from '@ohif/core';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory function that creates a new "Web API" data source.
|
* Factory function that creates a new "Web API" data source.
|
||||||
* A "Web API" data source is any source that fetches data over
|
* 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
|
* It's worth noting that a single implementation of this interface
|
||||||
* can define different underlying sources for "read" and "write" operations.
|
* can define different underlying sources for "read" and "write" operations.
|
||||||
*/
|
*/
|
||||||
function create({ query, retrieve, retrieveMetadata }) {
|
function create({ query, retrieve }) {
|
||||||
const defaultQuery = {
|
const defaultQuery = {
|
||||||
studies: {
|
studies: {
|
||||||
/**
|
/**
|
||||||
@ -31,12 +33,13 @@ function create({ query, retrieve, retrieveMetadata }) {
|
|||||||
instances: {},
|
instances: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultRetrieve = {};
|
const defaultRetrieve = {
|
||||||
|
series: {},
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
query,
|
query,
|
||||||
retrieve: {},
|
retrieve,
|
||||||
// retrieveMetadata: retrieveMetadata.bind(null, dicomMetadataStore);
|
|
||||||
// then go get all series level metadata.
|
// then go get all series level metadata.
|
||||||
// Store this in the DICOM MetadataStore.
|
// Store this in the DICOM MetadataStore.
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
92
platform/core/src/displaySetManager.js
Normal file
92
platform/core/src/displaySetManager.js
Normal file
@ -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;
|
||||||
@ -25,15 +25,15 @@ export default class ExtensionManager {
|
|||||||
*
|
*
|
||||||
* @param {Object[]} extensions - Array of extensions
|
* @param {Object[]} extensions - Array of extensions
|
||||||
*/
|
*/
|
||||||
registerExtensions = extensions => {
|
registerExtensions = (extensions, dataSources) => {
|
||||||
extensions.forEach(extension => {
|
extensions.forEach(extension => {
|
||||||
const hasConfiguration = Array.isArray(extension);
|
const hasConfiguration = Array.isArray(extension);
|
||||||
|
|
||||||
if (hasConfiguration) {
|
if (hasConfiguration) {
|
||||||
const [ohifExtension, configuration] = extension;
|
const [ohifExtension, configuration] = extension;
|
||||||
this.registerExtension(ohifExtension, configuration);
|
this.registerExtension(ohifExtension, dataSources, configuration);
|
||||||
} else {
|
} else {
|
||||||
this.registerExtension(extension);
|
this.registerExtension(extension, dataSources);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -44,7 +44,7 @@ export default class ExtensionManager {
|
|||||||
* @param {Object} extension
|
* @param {Object} extension
|
||||||
* @param {Object} configuration
|
* @param {Object} configuration
|
||||||
*/
|
*/
|
||||||
registerExtension = (extension, configuration = {}) => {
|
registerExtension = (extension, dataSources, configuration = {}) => {
|
||||||
if (!extension) {
|
if (!extension) {
|
||||||
log.warn(
|
log.warn(
|
||||||
'Attempting to register a null/undefined extension. Exiting early.'
|
'Attempting to register a null/undefined extension. Exiting early.'
|
||||||
@ -89,7 +89,12 @@ export default class ExtensionManager {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (extensionModule) {
|
if (extensionModule) {
|
||||||
this._initSpecialModuleTypes(moduleType, extensionModule);
|
this._initSpecialModuleTypes(
|
||||||
|
extensionId,
|
||||||
|
moduleType,
|
||||||
|
extensionModule,
|
||||||
|
dataSources
|
||||||
|
);
|
||||||
|
|
||||||
this.modules[moduleType].push({
|
this.modules[moduleType].push({
|
||||||
extensionId,
|
extensionId,
|
||||||
@ -112,9 +117,9 @@ export default class ExtensionManager {
|
|||||||
return this.modulesMap[stringEntry];
|
return this.modulesMap[stringEntry];
|
||||||
};
|
};
|
||||||
|
|
||||||
getDataSource = dataSourceId => {
|
getDataSources = dataSourceName => {
|
||||||
// Note: this currently uses the data source name, which feels weird...
|
// 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) {
|
switch (moduleType) {
|
||||||
case 'commandsModule': {
|
case 'commandsModule': {
|
||||||
const { definitions, defaultContext } = extensionModule;
|
const { definitions, defaultContext } = extensionModule;
|
||||||
@ -168,7 +178,25 @@ export default class ExtensionManager {
|
|||||||
}
|
}
|
||||||
case 'dataSourcesModule': {
|
case 'dataSourcesModule': {
|
||||||
extensionModule.forEach(element => {
|
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;
|
break;
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import studies from './studies/';
|
|||||||
import ui from './ui';
|
import ui from './ui';
|
||||||
import user from './user.js';
|
import user from './user.js';
|
||||||
import dicomMetadataStore from './dicomMetadataStore';
|
import dicomMetadataStore from './dicomMetadataStore';
|
||||||
|
import displaySetManager from './displaySetManager';
|
||||||
import utils, { hotkeys } from './utils/';
|
import utils, { hotkeys } from './utils/';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -66,6 +67,7 @@ const OHIF = {
|
|||||||
MeasurementService,
|
MeasurementService,
|
||||||
IWebApiDataSource,
|
IWebApiDataSource,
|
||||||
dicomMetadataStore,
|
dicomMetadataStore,
|
||||||
|
displaySetManager,
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -102,6 +104,7 @@ export {
|
|||||||
MeasurementService,
|
MeasurementService,
|
||||||
IWebApiDataSource,
|
IWebApiDataSource,
|
||||||
dicomMetadataStore,
|
dicomMetadataStore,
|
||||||
|
displaySetManager,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { OHIF };
|
export { OHIF };
|
||||||
|
|||||||
@ -7,8 +7,9 @@ window.config = {
|
|||||||
// filterQueryParam: false,
|
// filterQueryParam: false,
|
||||||
dataSources: [
|
dataSources: [
|
||||||
{
|
{
|
||||||
friendlyName: "Rodrigo's DICOMWeb Server",
|
friendlyName: 'dcmjs DICOMWeb Server',
|
||||||
namespace: 'org.ohif.default.datasources.dicomweb',
|
namespace: 'org.ohif.default.dataSourcesModule.dicomweb',
|
||||||
|
sourceName: 'dicomweb',
|
||||||
configuration: {
|
configuration: {
|
||||||
name: 'DCM4CHEE',
|
name: 'DCM4CHEE',
|
||||||
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
|
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
|
||||||
|
|||||||
@ -50,10 +50,10 @@ function appInit(appConfigOrFunc, defaultExtensions) {
|
|||||||
* Example: [ext1, ext2, ext3]
|
* Example: [ext1, ext2, ext3]
|
||||||
* Example2: [[ext1, config], ext2, [ext3, config]]
|
* Example2: [[ext1, config], ext2, [ext3, config]]
|
||||||
*/
|
*/
|
||||||
extensionManager.registerExtensions([
|
extensionManager.registerExtensions(
|
||||||
...defaultExtensions,
|
[...defaultExtensions, ...appConfig.extensions],
|
||||||
...appConfig.extensions,
|
appConfig.dataSources
|
||||||
]);
|
);
|
||||||
|
|
||||||
// TODO: Init global hotkeys, or the hotkeys manager?
|
// TODO: Init global hotkeys, or the hotkeys manager?
|
||||||
// TODO: We no longer use `utils.addServer`
|
// TODO: We no longer use `utils.addServer`
|
||||||
@ -69,7 +69,8 @@ function appInit(appConfigOrFunc, defaultExtensions) {
|
|||||||
modes.push(window.exampleMode);
|
modes.push(window.exampleMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
const appRoutes = buildModeRoutes(modes, extensionManager) || [];
|
const appRoutes =
|
||||||
|
buildModeRoutes(modes, appConfig.dataSources, extensionManager) || [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appConfig,
|
appConfig,
|
||||||
|
|||||||
@ -1,20 +1,26 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import SOPClassHandlerManager from './SOPClassHandlerManager';
|
import { displaySetManager } from '@ohif/core';
|
||||||
import ViewModelContext from './ViewModelContext';
|
import ViewModelContext from './ViewModelContext';
|
||||||
import Compose from './Compose';
|
import Compose from './Compose';
|
||||||
|
|
||||||
export default function ModeRoute({
|
export default function ModeRoute({
|
||||||
location,
|
location,
|
||||||
mode,
|
mode,
|
||||||
dataSourceId,
|
dataSourceName,
|
||||||
extensionManager,
|
extensionManager,
|
||||||
}) {
|
}) {
|
||||||
const { routes, sopClassHandlers, extensions } = mode;
|
const { routes, sopClassHandlers, extensions } = mode;
|
||||||
const dataSource = extensionManager.getDataSource(dataSourceId);
|
const dataSources = extensionManager.getDataSources(dataSourceName);
|
||||||
|
|
||||||
const { displaySetInstanceUids, setDisplaySetInstanceUids } = useContext(
|
// TODO: For now assume one unique datasource.
|
||||||
ViewModelContext
|
|
||||||
);
|
const dataSource = dataSources[0];
|
||||||
|
|
||||||
|
const viewModelContext = useContext(ViewModelContext);
|
||||||
|
|
||||||
|
console.log(viewModelContext);
|
||||||
|
|
||||||
|
const { displaySetInstanceUids } = viewModelContext;
|
||||||
// Deal with toolbar.
|
// Deal with toolbar.
|
||||||
|
|
||||||
// Only handling one route per mode for now
|
// Only handling one route per mode for now
|
||||||
@ -28,28 +34,25 @@ export default function ModeRoute({
|
|||||||
const LayoutComponent = layoutTemplateModuleEntry.component;
|
const LayoutComponent = layoutTemplateModuleEntry.component;
|
||||||
|
|
||||||
// Add SOPClassHandlers to a new SOPClassManager.
|
// Add SOPClassHandlers to a new SOPClassManager.
|
||||||
const manager = new SOPClassHandlerManager(
|
|
||||||
extensionManager,
|
|
||||||
sopClassHandlers
|
|
||||||
);
|
|
||||||
|
|
||||||
/*setInterval(() => {
|
debugger;
|
||||||
setDisplaySetInstanceUids(manager.displaySets);
|
|
||||||
}, 5000);*/
|
displaySetManager.init(extensionManager, sopClassHandlers, viewModelContext);
|
||||||
|
|
||||||
|
// setInterval(() => {
|
||||||
|
// setDisplaySetInstanceUids(manager.displaySets);
|
||||||
|
// }, 5000);
|
||||||
|
|
||||||
const queryParams = location.search;
|
const queryParams = location.search;
|
||||||
|
|
||||||
// Call the data source to start building the view model?
|
// Call the data source to start building the view model?
|
||||||
//dataSource(queryParams);
|
dataSource.retrieve.series.metadata(
|
||||||
//metadataStore.onModified();
|
queryParams,
|
||||||
|
displaySetManager.makeDisplaySets
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
// For each extension, look up their context modules
|
// For each extension, look up their context modules
|
||||||
|
// TODO: move to extension manager.
|
||||||
let contextModules = [];
|
let contextModules = [];
|
||||||
extensions.forEach(extensionId => {
|
extensions.forEach(extensionId => {
|
||||||
const allRegisteredModuleIds = Object.keys(extensionManager.modulesMap);
|
const allRegisteredModuleIds = Object.keys(extensionManager.modulesMap);
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -22,28 +22,35 @@ import { ViewModelProvider } from './ViewModelContext';
|
|||||||
|
|
||||||
/:modeId/:modeRoute/?queryParameters=example
|
/:modeId/:modeRoute/?queryParameters=example
|
||||||
*/
|
*/
|
||||||
export default function buildModeRoutes(modes, extensionManager) {
|
export default function buildModeRoutes(modes, dataSources, extensionManager) {
|
||||||
const routes = [];
|
const routes = [];
|
||||||
|
|
||||||
// TODO: Build api for this.
|
// const dataSources = Object.keys(extensionManager.dataSourceMap).map(a =>
|
||||||
// Currently builds an endpoint for all data sources, which probably
|
// extensionManager.getDataSources(a)
|
||||||
// doesn't make sense
|
// );
|
||||||
const dataSources = Object.keys(extensionManager.dataSourceMap).map(a =>
|
|
||||||
extensionManager.getDataSource(a)
|
const dataSourceNames = [];
|
||||||
);
|
|
||||||
|
dataSources.forEach(dataSource => {
|
||||||
|
const { sourceName } = dataSource;
|
||||||
|
if (!dataSourceNames.includes(sourceName)) {
|
||||||
|
dataSourceNames.push(sourceName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
modes.forEach(mode => {
|
modes.forEach(mode => {
|
||||||
dataSources.forEach(dataSource => {
|
// todo: for each route. add route to path.
|
||||||
|
dataSourceNames.forEach(dataSourceName => {
|
||||||
// TODO: name vs id
|
// TODO: name vs id
|
||||||
const dataSourceId = dataSource.name;
|
const path = `/${mode.id}/${dataSourceName}`;
|
||||||
const path = `/${mode.id}/${dataSourceId}`;
|
|
||||||
|
|
||||||
|
// TODO move up.
|
||||||
const component = ({ location }) => (
|
const component = ({ location }) => (
|
||||||
<ViewModelProvider>
|
<ViewModelProvider>
|
||||||
<ModeRoute
|
<ModeRoute
|
||||||
location={location}
|
location={location}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
dataSourceId={dataSourceId}
|
dataSourceName={dataSourceName}
|
||||||
extensionManager={extensionManager}
|
extensionManager={extensionManager}
|
||||||
/>
|
/>
|
||||||
</ViewModelProvider>
|
</ViewModelProvider>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user