Improving new study browser and moving studies services to ohif:studies
This commit is contained in:
1 parent
0b51a53976
commit
eb372bed03
54 files changed
+560
-495
No files matched your search
+1
-1
@@ -36,7 +36,7 @@ Template.seriesDetailsTable.onRendered(() => {
|
||||
// Get series list for the study
|
||||
_.map(studies, (selectedStudy, index) => {
|
||||
studies[index].seriesList = [];
|
||||
OHIF.studylist.retrieveStudyMetadata(study => {
|
||||
OHIF.studies.retrieveStudyMetadata(study => {
|
||||
// Set series list
|
||||
studies[index].seriesList = study.seriesList;
|
||||
studies[index].displaySeriesLoadingText = false;
|
||||
|
||||
@@ -13,7 +13,7 @@ export class OHIFStudyMetadataSource extends OHIF.viewerbase.StudyMetadataSource
|
||||
* @return {Promise} A Promise object
|
||||
*/
|
||||
getByInstanceUID(studyInstanceUID) {
|
||||
return OHIF.studylist.retrieveStudyMetadata(studyInstanceUID);
|
||||
return OHIF.studies.retrieveStudyMetadata(studyInstanceUID);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,7 +12,7 @@ queryStudies = function(studiesToQuery, options) {
|
||||
const promises = [];
|
||||
|
||||
studiesToQuery.forEach(studyToQuery => {
|
||||
const promise = OHIF.studylist.retrieveStudyMetadata(studyToQuery.studyInstanceUid);
|
||||
const promise = OHIF.studies.retrieveStudyMetadata(studyToQuery.studyInstanceUid);
|
||||
promise.then(study => {
|
||||
studiesQueried++;
|
||||
notify({
|
||||
|
||||
@@ -1,42 +1,12 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
const note = 'OHIF.studylist.retrieveStudiesMetadata is deprecated.';
|
||||
const instructions = 'Please use OHIF.studies.retrieveStudiesMetadata instead.';
|
||||
|
||||
/**
|
||||
* Retrieves metaData for multiple studies at once.
|
||||
*
|
||||
* This function calls retrieveStudyMetadata several times, asynchronously,
|
||||
* and waits for all of the results to be returned.
|
||||
*
|
||||
* @param studyInstanceUids The UIDs of the Studies to be retrieved
|
||||
* @return Promise
|
||||
* @deprecated Please use OHIF.studies.retrieveStudiesMetadata instead
|
||||
*/
|
||||
OHIF.studylist.retrieveStudiesMetadata = (studyInstanceUids, seriesInstanceUids, doneCallback, failCallback) => {
|
||||
// // Check to make sure studyInstanceUids were actually input
|
||||
// if (!studyInstanceUids || !studyInstanceUids.length) {
|
||||
// if (failCallback && typeof failCallback === 'function') {
|
||||
// failCallback('No studyInstanceUids were input');
|
||||
// }
|
||||
//
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Create an empty array to store the Promises for each metaData retrieval call
|
||||
const promises = [];
|
||||
|
||||
// Loop through the array of studyInstanceUids
|
||||
studyInstanceUids.forEach(function(studyInstanceUid) {
|
||||
// Send the call, and attach doneCallbacks and failCallbacks
|
||||
// which can resolve or reject the related promise based on its outcome
|
||||
const promise = OHIF.studylist.retrieveStudyMetadata(studyInstanceUid, seriesInstanceUids);
|
||||
|
||||
// Add the current promise to the array of promises
|
||||
promises.push(promise);
|
||||
});
|
||||
|
||||
// When all of the promises are complete, this callback runs
|
||||
const promise = Promise.all(promises);
|
||||
|
||||
// Warn the error on console if some retrieval failed
|
||||
promise.catch(error => OHIF.log.warn(error));
|
||||
|
||||
return promise;
|
||||
OHIF.studylist.retrieveStudiesMetadata = function() {
|
||||
OHIF.log.warn(`${note}\n${instructions}`);
|
||||
OHIF.studies.retrieveStudiesMetadata.apply(this, arguments);
|
||||
};
|
||||
@@ -1,110 +1,12 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import 'meteor/ohif:viewerbase';
|
||||
|
||||
// Define the StudyMetaDataPromises object. This is used as a cache to store study meta data
|
||||
// promises and prevent unnecessary subsequent calls to the server
|
||||
const StudyMetaDataPromises = new Map();
|
||||
const note = 'OHIF.studylist.retrieveStudyMetadata is deprecated.';
|
||||
const instructions = 'Please use OHIF.studies.retrieveStudyMetadata instead.';
|
||||
|
||||
/**
|
||||
* Retrieves study metadata using a server call
|
||||
*
|
||||
* @param {String} studyInstanceUid The UID of the Study to be retrieved
|
||||
* @returns {Promise} that will be resolved with the metadata or rejected with the error
|
||||
* @deprecated Please use OHIF.studies.retrieveStudyMetadata instead
|
||||
*/
|
||||
OHIF.studylist.retrieveStudyMetadata = (studyInstanceUid, seriesInstanceUids) => {
|
||||
|
||||
// @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 the StudyMetaDataPromises cache already has a pending or resolved promise related to the
|
||||
// given studyInstanceUid, then that promise is returned
|
||||
if (StudyMetaDataPromises.has(studyInstanceUid)) {
|
||||
return StudyMetaDataPromises.get(studyInstanceUid);
|
||||
}
|
||||
|
||||
console.time('retrieveStudyMetadata');
|
||||
|
||||
// Create a promise to handle the data retrieval
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
// If no study metadata is in the cache variable, we need to retrieve it from
|
||||
// the server with a call.
|
||||
Meteor.call('GetStudyMetadata', studyInstanceUid, function(error, study) {
|
||||
console.timeEnd('retrieveStudyMetadata');
|
||||
|
||||
if (error) {
|
||||
const errorType = error.error;
|
||||
let errorMessage = '';
|
||||
|
||||
if (errorType === 'server-connection-error') {
|
||||
errorMessage = 'There was an error connecting to the DICOM server, please verify if it is up and running.'
|
||||
} else if (errorType === 'server-internal-error') {
|
||||
errorMessage = `There was an internal error with the DICOM server getting metadeta for ${studyInstanceUid}`;
|
||||
} else {
|
||||
errorMessage = `For some reason we could not retrieve the study\'s metadata for ${studyInstanceUid}.`;
|
||||
}
|
||||
|
||||
OHIF.log.error(errorMessage);
|
||||
OHIF.log.error(error.stack);
|
||||
reject(`GetStudyMetadata: ${errorMessage}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter series if seriesInstanceUid exists
|
||||
if (seriesInstanceUids && seriesInstanceUids.length) {
|
||||
study.seriesList = study.seriesList.filter(series => seriesInstanceUids.indexOf(series.seriesInstanceUid) > -1);
|
||||
}
|
||||
|
||||
if (!study) {
|
||||
reject(`GetStudyMetadata: No study data returned from server: ${studyInstanceUid}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.HipaaLogger && Meteor.user && Meteor.user()) {
|
||||
window.HipaaLogger.logEvent({
|
||||
eventType: 'viewed',
|
||||
userId: Meteor.userId(),
|
||||
userName: Meteor.user().profile.fullName,
|
||||
collectionName: 'Study',
|
||||
recordId: studyInstanceUid,
|
||||
patientId: study.patientId,
|
||||
patientName: study.patientName
|
||||
});
|
||||
}
|
||||
|
||||
// Once the data was retrieved, the series are sorted by series and instance number
|
||||
OHIF.viewerbase.sortStudy(study);
|
||||
|
||||
// Updates WADO-RS metaDataManager
|
||||
OHIF.viewerbase.updateMetaDataManager(study);
|
||||
|
||||
// Add additional metadata to our study from the studylist
|
||||
const studylistStudy = OHIF.studylist.collections.Studies.findOne({
|
||||
studyInstanceUid: study.studyInstanceUid,
|
||||
'seriesList.seriesInstanceUid': '123456',
|
||||
});
|
||||
|
||||
if (studylistStudy) {
|
||||
Object.assign(study, studylistStudy);
|
||||
}
|
||||
|
||||
// Transform the study in a StudyMetadata object
|
||||
const studyMetadata = new OHIF.metadata.StudyMetadata(study);
|
||||
|
||||
// Add the display sets to the study
|
||||
study.displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(studyMetadata);
|
||||
study.displaySets.forEach(displaySet => {
|
||||
OHIF.viewerbase.stackManager.makeAndAddStack(study, displaySet);
|
||||
});
|
||||
|
||||
// Resolve the promise with the final study metadata object
|
||||
resolve(study);
|
||||
});
|
||||
});
|
||||
|
||||
// Store the promise in cache
|
||||
StudyMetaDataPromises.set(studyInstanceUid, promise);
|
||||
|
||||
return promise;
|
||||
OHIF.studylist.retrieveStudyMetadata = function() {
|
||||
OHIF.log.warn(`${note}\n${instructions}`);
|
||||
OHIF.studies.retrieveStudyMetadata.apply(this, arguments);
|
||||
};
|
||||
@@ -44,6 +44,4 @@ Package.onUse(function(api) {
|
||||
|
||||
// Client imports
|
||||
api.addFiles('client/index.js', 'client');
|
||||
|
||||
api.export('Services', 'server');
|
||||
});
|
||||
@@ -1,5 +1 @@
|
||||
import './publications.js';
|
||||
|
||||
import './lib';
|
||||
import './methods';
|
||||
import './services';
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Converts the properties to URL query parameters. Based on:
|
||||
* http://stackoverflow.com/questions/111529/create-query-parameters-in-javascript
|
||||
*
|
||||
* @param data
|
||||
* @returns {string}
|
||||
*/
|
||||
encodeQueryData = function(data) {
|
||||
var ret = [];
|
||||
|
||||
for (var d in data) {
|
||||
if (data[d]) {
|
||||
ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d]));
|
||||
}
|
||||
}
|
||||
|
||||
return ret.join('&');
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
import './remoteGetValue.js';
|
||||
import './encodeQueryData.js';
|
||||
@@ -1,14 +0,0 @@
|
||||
export const parseFloatArray = function(obj) {
|
||||
var result = [];
|
||||
|
||||
if (!obj) {
|
||||
return result;
|
||||
}
|
||||
|
||||
var objs = obj.split("\\");
|
||||
for (var i = 0; i < objs.length; i++) {
|
||||
result.push(parseFloat(objs[i]));
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export const remoteGetValue = function(obj) {
|
||||
return obj ? obj.Value : null;
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Meteor.methods({
|
||||
/**
|
||||
* Retrieves Study metadata given a Study Instance UID
|
||||
* This Meteor method is available from both the client and the server
|
||||
*/
|
||||
GetStudyMetadata: function(studyInstanceUid) {
|
||||
OHIF.log.info('GetStudyMetadata(%s)', studyInstanceUid);
|
||||
|
||||
// Get the server data. This is user-defined in the config.json files or through servers
|
||||
// configuration modal
|
||||
const server = OHIF.servers.getCurrentServer();
|
||||
|
||||
if (!server) {
|
||||
throw new Meteor.Error('improper-server-config', 'No properly configured server was available over DICOMWeb or DIMSE.');
|
||||
}
|
||||
|
||||
try {
|
||||
if (server.type === 'dicomWeb') {
|
||||
return Services.WADO.RetrieveMetadata(server, studyInstanceUid);
|
||||
} else if (server.type === 'dimse') {
|
||||
return Services.DIMSE.RetrieveMetadata(studyInstanceUid);
|
||||
}
|
||||
} catch (error) {
|
||||
OHIF.log.trace();
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,3 +1,2 @@
|
||||
import './getStudyMetadata.js';
|
||||
import './importStudies.js';
|
||||
import './studylistSearch.js';
|
||||
@@ -18,9 +18,9 @@ Meteor.methods({
|
||||
|
||||
try {
|
||||
if (server.type === 'dicomWeb') {
|
||||
return Services.QIDO.Studies(server, filter);
|
||||
return OHIF.studies.services.QIDO.Studies(server, filter);
|
||||
} else if (server.type === 'dimse') {
|
||||
return Services.DIMSE.Studies(filter);
|
||||
return OHIF.studies.services.DIMSE.Studies(filter);
|
||||
}
|
||||
} catch (error) {
|
||||
OHIF.log.trace();
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
/**
|
||||
* Parses data returned from a study search and transforms it into
|
||||
* an array of series that are present in the study
|
||||
*
|
||||
* @param resultData
|
||||
* @returns {Array} Series List
|
||||
*/
|
||||
function resultDataToStudyMetadata(resultData, studyInstanceUid) {
|
||||
const seriesMap = {};
|
||||
const seriesList = [];
|
||||
|
||||
resultData.forEach(function(instanceRaw) {
|
||||
const instance = instanceRaw.toObject();
|
||||
// Use seriesMap to cache series data
|
||||
// If the series instance UID has already been used to
|
||||
// process series data, continue using that series
|
||||
const seriesInstanceUid = instance[0x0020000E];
|
||||
let series = seriesMap[seriesInstanceUid];
|
||||
|
||||
// If no series data exists in the seriesMap cache variable,
|
||||
// process any available series data
|
||||
if (!series) {
|
||||
series = {
|
||||
seriesInstanceUid: seriesInstanceUid,
|
||||
seriesNumber: instance[0x00200011],
|
||||
instances: []
|
||||
};
|
||||
|
||||
// Save this data in the seriesMap cache variable
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
// TODO: Check which peer it should point to
|
||||
const server = OHIF.servers.getCurrentServer().peers[0];
|
||||
|
||||
const serverRoot = server.host + ':' + server.port;
|
||||
|
||||
const sopInstanceUid = instance[0x00080018];
|
||||
const uri = serverRoot + '/studies/' + studyInstanceUid + '/series/' + seriesInstanceUid + '/instances/' + sopInstanceUid + '/frames/1';
|
||||
|
||||
// Add this instance to the current series
|
||||
series.instances.push({
|
||||
sopClassUid: instance[0x00080016],
|
||||
sopInstanceUid,
|
||||
uri,
|
||||
instanceNumber: instance[0x00200013]
|
||||
});
|
||||
});
|
||||
return seriesList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a set of instances using a DIMSE call
|
||||
* @param studyInstanceUid
|
||||
* @returns {{wadoUriRoot: String, studyInstanceUid: String, seriesList: Array}}
|
||||
*/
|
||||
Services.DIMSE.Instances = function(studyInstanceUid) {
|
||||
//var url = buildUrl(server, studyInstanceUid);
|
||||
const result = DIMSE.retrieveInstances(studyInstanceUid);
|
||||
|
||||
return {
|
||||
studyInstanceUid: studyInstanceUid,
|
||||
seriesList: resultDataToStudyMetadata(result, studyInstanceUid)
|
||||
};
|
||||
};
|
||||
@@ -1,169 +0,0 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { parseFloatArray } from 'meteor/ohif:study-list/server/lib/parseFloatArray';
|
||||
|
||||
/**
|
||||
* Returns the value of the element (e.g. '00280009')
|
||||
*
|
||||
* @param element - The group/element of the element (e.g. '00280009')
|
||||
* @param defaultValue - The default value to return if the element does not exist
|
||||
* @returns {*}
|
||||
*/
|
||||
function getValue(element, defaultValue) {
|
||||
if (!element || !element.value) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return element.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
const SourceImageSequence = instance[0x00082112];
|
||||
if (SourceImageSequence && SourceImageSequence.length) {
|
||||
return SourceImageSequence[0][0x00081155];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses result data from a DIMSE search into Study MetaData
|
||||
* Returns an object populated with study metadata, including the
|
||||
* series list.
|
||||
*
|
||||
* @param studyInstanceUid
|
||||
* @param resultData
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
function resultDataToStudyMetadata(studyInstanceUid, resultData) {
|
||||
OHIF.log.info('resultDataToStudyMetadata');
|
||||
const seriesMap = {};
|
||||
const seriesList = [];
|
||||
|
||||
if (!resultData.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const anInstance = resultData[0].toObject();
|
||||
if (!anInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
const studyData = {
|
||||
seriesList: seriesList,
|
||||
patientName: anInstance[0x00100010],
|
||||
patientId: anInstance[0x00100020],
|
||||
patientBirthDate: anInstance[0x00100030],
|
||||
patientSex: anInstance[0x00100040],
|
||||
accessionNumber: anInstance[0x00080050],
|
||||
studyDate: anInstance[0x00080020],
|
||||
modalities: anInstance[0x00080061],
|
||||
studyDescription: anInstance[0x00081030],
|
||||
imageCount: anInstance[0x00201208],
|
||||
studyInstanceUid: anInstance[0x0020000D],
|
||||
institutionName: anInstance[0x00080080]
|
||||
};
|
||||
|
||||
resultData.forEach(function(instanceRaw) {
|
||||
const instance = instanceRaw.toObject();
|
||||
const seriesInstanceUid = instance[0x0020000E];
|
||||
let series = seriesMap[seriesInstanceUid];
|
||||
if (!series) {
|
||||
series = {
|
||||
seriesDescription: instance[0x0008103E],
|
||||
modality: instance[0x00080060],
|
||||
seriesInstanceUid: seriesInstanceUid,
|
||||
seriesNumber: parseFloat(instance[0x00200011]),
|
||||
instances: []
|
||||
};
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
const sopInstanceUid = instance[0x00080018];
|
||||
|
||||
const instanceSummary = {
|
||||
imageType: instance[0x00080008],
|
||||
sopClassUid: instance[0x00080016],
|
||||
modality: instance[0x00080060],
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
instanceNumber: parseFloat(instance[0x00200013]),
|
||||
imagePositionPatient: instance[0x00200032],
|
||||
imageOrientationPatient: instance[0x00200037],
|
||||
frameOfReferenceUID: instance[0x00200052],
|
||||
sliceThickness: parseFloat(instance[0x00180050]),
|
||||
sliceLocation: parseFloat(instance[0x00201041]),
|
||||
tablePosition: parseFloat(instance[0x00189327]),
|
||||
samplesPerPixel: parseFloat(instance[0x00280002]),
|
||||
photometricInterpretation: instance[0x00280004],
|
||||
planarConfiguration: parseFloat(instance[0x00280006]),
|
||||
rows: parseFloat(instance[0x00280010]),
|
||||
columns: parseFloat(instance[0x00280011]),
|
||||
pixelSpacing: instance[0x00280030],
|
||||
bitsAllocated: parseFloat(instance[0x00280100]),
|
||||
bitsStored: parseFloat(instance[0x00280101]),
|
||||
highBit: parseFloat(instance[0x00280102]),
|
||||
pixelRepresentation: parseFloat(instance[0x00280103]),
|
||||
windowCenter: instance[0x00281050],
|
||||
windowWidth: instance[0x00281051],
|
||||
rescaleIntercept: parseFloat(instance[0x00281052]),
|
||||
rescaleSlope: parseFloat(instance[0x00281053]),
|
||||
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
|
||||
laterality: instance[0x00200062],
|
||||
viewPosition: instance[0x00185101],
|
||||
acquisitionDateTime: instance[0x0008002A],
|
||||
numberOfFrames: parseFloat(instance[0x00280008]),
|
||||
frameIncrementPointer: getValue(instance[0x00280009]),
|
||||
frameTime: parseFloat(instance[0x00181063]),
|
||||
frameTimeVector: parseFloatArray(instance[0x00181065]),
|
||||
lossyImageCompression: instance[0x00282110],
|
||||
derivationDescription: instance[0x00282111],
|
||||
lossyImageCompressionRatio: instance[0x00282112],
|
||||
lossyImageCompressionMethod: instance[0x00282114],
|
||||
spacingBetweenSlices: instance[0x00180088],
|
||||
echoNumber: instance[0x00180086],
|
||||
contrastBolusAgent: instance[0x00180010]
|
||||
};
|
||||
|
||||
// Retrieve the actual data over WADO-URI
|
||||
const server = OHIF.servers.getCurrentServer();
|
||||
const wadouri = `${server.wadoUriRoot}?requestType=WADO&studyUID=${studyInstanceUid}&seriesUID=${seriesInstanceUid}&objectUID=${sopInstanceUid}&contentType=application%2Fdicom`;
|
||||
instanceSummary.wadouri = WADOProxy.convertURL(wadouri, server);
|
||||
|
||||
series.instances.push(instanceSummary);
|
||||
});
|
||||
|
||||
studyData.studyInstanceUid = studyInstanceUid;
|
||||
|
||||
return studyData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieved Study MetaData from a DICOM server using DIMSE
|
||||
* @param studyInstanceUid
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
Services.DIMSE.RetrieveMetadata = function(studyInstanceUid) {
|
||||
// TODO: Check which peer it should point to
|
||||
const activeServer = OHIF.servers.getCurrentServer().peers[0];
|
||||
const supportsInstanceRetrievalByStudyUid = activeServer.supportsInstanceRetrievalByStudyUid;
|
||||
let results;
|
||||
|
||||
// Check explicitly for a value of false, since this property
|
||||
// may be left undefined in config files
|
||||
if (supportsInstanceRetrievalByStudyUid === false) {
|
||||
results = DIMSE.retrieveInstancesByStudyOnly(studyInstanceUid);
|
||||
} else {
|
||||
results = DIMSE.retrieveInstances(studyInstanceUid);
|
||||
}
|
||||
|
||||
return resultDataToStudyMetadata(studyInstanceUid, results);
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { CurrentServer } from 'meteor/ohif:servers/both/collections';
|
||||
|
||||
const setupDIMSE = () => {
|
||||
// Terminate existing DIMSE servers and sockets and clean up the connection object
|
||||
DIMSE.connection.reset();
|
||||
|
||||
// Get the new server configuration
|
||||
const server = OHIF.servers.getCurrentServer();
|
||||
|
||||
// Stop here if the new server is not of DIMSE type
|
||||
if (server.type !== 'dimse') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if peers were defined in the server configuration and throw an error if not
|
||||
const peers = server.peers;
|
||||
if (!peers || !peers.length) {
|
||||
OHIF.log.error('dimse-config: ' + 'No DIMSE Peers provided.');
|
||||
throw new Meteor.Error('dimse-config', 'No DIMSE Peers provided.');
|
||||
}
|
||||
|
||||
// Add all the DIMSE peers, establishing the connections
|
||||
OHIF.log.info('Adding DIMSE peers');
|
||||
try {
|
||||
peers.forEach(peer => DIMSE.connection.addPeer(peer));
|
||||
} catch(error) {
|
||||
OHIF.log.error('dimse-addPeers: ' + error);
|
||||
throw new Meteor.Error('dimse-addPeers', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Setup the DIMSE connections on startup or when the current server is changed
|
||||
Meteor.startup(() => {
|
||||
CurrentServer.find().observe({
|
||||
added: setupDIMSE,
|
||||
changed: setupDIMSE
|
||||
});
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
/**
|
||||
* Parses resulting data from a QIDO call into a set of Study MetaData
|
||||
*
|
||||
* @param resultData
|
||||
* @returns {Array} An array of Study MetaData objects
|
||||
*/
|
||||
function resultDataToStudies(resultData) {
|
||||
var studies = [];
|
||||
|
||||
resultData.forEach(function(studyRaw) {
|
||||
var study = studyRaw.toObject();
|
||||
studies.push({
|
||||
studyInstanceUid: study[0x0020000D],
|
||||
// 00080005 = SpecificCharacterSet
|
||||
studyDate: study[0x00080020],
|
||||
studyTime: study[0x00080030],
|
||||
accessionNumber: study[0x00080050],
|
||||
referringPhysicianName: study[0x00080090],
|
||||
// 00081190 = URL
|
||||
patientName: study[0x00100010],
|
||||
patientId: study[0x00100020],
|
||||
patientBirthdate: study[0x00100030],
|
||||
patientSex: study[0x00100040],
|
||||
imageCount: study[0x00201208],
|
||||
studyId: study[0x00200010],
|
||||
studyDescription: study[0x00081030],
|
||||
modalities: study[0x00080061]
|
||||
});
|
||||
});
|
||||
return studies;
|
||||
}
|
||||
|
||||
Services.DIMSE.Studies = function(filter) {
|
||||
OHIF.log.info('Services.DIMSE.Studies');
|
||||
|
||||
let filterStudyDate = '';
|
||||
if (filter.studyDateFrom && filter.studyDateTo) {
|
||||
const convertDate = date => moment(date, 'MM/DD/YYYY').format('YYYYMMDD');
|
||||
const dateFrom = convertDate(filter.studyDateFrom);
|
||||
const dateTo = convertDate(filter.studyDateTo);
|
||||
filterStudyDate = `${dateFrom}-${dateTo}`;
|
||||
}
|
||||
|
||||
var parameters = {
|
||||
0x00100010: filter.patientName,
|
||||
0x00100020: filter.patientId,
|
||||
0x00080050: filter.accessionNumber,
|
||||
0x00080020: filterStudyDate,
|
||||
0x00081030: filter.studyDescription,
|
||||
0x00100040: '',
|
||||
0x00201208: '',
|
||||
0x00080061: filter.modalitiesInStudy
|
||||
};
|
||||
|
||||
var results = DIMSE.retrieveStudies(parameters);
|
||||
return resultDataToStudies(results);
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import './namespace.js';
|
||||
|
||||
// DICOMWeb instance, study, and metadata retrieval
|
||||
import './qido/instances.js';
|
||||
import './qido/studies.js';
|
||||
import './wado/retrieveMetadata.js';
|
||||
|
||||
// DIMSE instance, study, and metadata retrieval
|
||||
import './dimse/instances.js';
|
||||
import './dimse/studies.js';
|
||||
import './dimse/retrieveMetadata.js';
|
||||
import './dimse/setup.js';
|
||||
|
||||
// Study, instance, and metadata retrieval from remote PACS via Orthanc as a proxy
|
||||
import './remote/instances.js';
|
||||
import './remote/studies.js';
|
||||
import './remote/retrieveMetadata.js';
|
||||
@@ -1,9 +0,0 @@
|
||||
Services = {};
|
||||
Services.QIDO = {};
|
||||
Services.WADO = {};
|
||||
Services.DIMSE = {};
|
||||
Services.REMOTE = {};
|
||||
|
||||
remoteGetValue = function(obj) {
|
||||
return obj ? obj.Value : null;
|
||||
};
|
||||
@@ -1,94 +0,0 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
/**
|
||||
* Creates a QIDO URL given the server settings and a study instance UID
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {string} URL to be used for QIDO calls
|
||||
*/
|
||||
function buildUrl(server, studyInstanceUid) {
|
||||
return server.qidoRoot + '/studies/' + studyInstanceUid + '/instances';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses data returned from a QIDO search and transforms it into
|
||||
* an array of series that are present in the study
|
||||
*
|
||||
* @param server The DICOM server
|
||||
* @param studyInstanceUid
|
||||
* @param resultData
|
||||
* @returns {Array} Series List
|
||||
*/
|
||||
function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
|
||||
resultData.forEach(function(instance) {
|
||||
// Use seriesMap to cache series data
|
||||
// If the series instance UID has already been used to
|
||||
// process series data, continue using that series
|
||||
var seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
|
||||
var series = seriesMap[seriesInstanceUid];
|
||||
|
||||
// If no series data exists in the seriesMap cache variable,
|
||||
// process any available series data
|
||||
if (!series) {
|
||||
series = {
|
||||
seriesInstanceUid: seriesInstanceUid,
|
||||
seriesNumber: DICOMWeb.getString(instance['00200011']),
|
||||
instances: []
|
||||
};
|
||||
|
||||
// Save this data in the seriesMap cache variable
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
// The uri for the dicomweb
|
||||
// NOTE: DCM4CHEE seems to return the data zipped
|
||||
// NOTE: Orthanc returns the data with multi-part mime which cornerstoneWADOImageLoader doesn't
|
||||
// know how to parse yet
|
||||
//var uri = DICOMWeb.getString(instance['00081190']);
|
||||
//uri = uri.replace('wado-rs', 'dicom-web');
|
||||
|
||||
// manually create a WADO-URI from the UIDs
|
||||
// NOTE: Haven't been able to get Orthanc's WADO-URI to work yet - maybe its not configured?
|
||||
var sopInstanceUid = DICOMWeb.getString(instance['00080018']);
|
||||
var uri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + '&contentType=application%2Fdicom';
|
||||
|
||||
// Add this instance to the current series
|
||||
series.instances.push({
|
||||
sopClassUid: DICOMWeb.getString(instance['00080016']),
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
uri: uri,
|
||||
instanceNumber: DICOMWeb.getString(instance['00200013'])
|
||||
});
|
||||
});
|
||||
return seriesList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a set of instances using a QIDO call
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @throws ECONNREFUSED
|
||||
* @returns {{wadoUriRoot: String, studyInstanceUid: String, seriesList: Array}}
|
||||
*/
|
||||
Services.QIDO.Instances = function(server, studyInstanceUid) {
|
||||
var url = buildUrl(server, studyInstanceUid);
|
||||
|
||||
try {
|
||||
var result = DICOMWeb.getJSON(url, server.requestOptions);
|
||||
|
||||
return {
|
||||
wadoUriRoot: server.wadoUriRoot,
|
||||
studyInstanceUid: studyInstanceUid,
|
||||
seriesList: resultDataToStudyMetadata(server, studyInstanceUid, result.data)
|
||||
};
|
||||
} catch (error) {
|
||||
OHIF.log.trace();
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,106 +0,0 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
/**
|
||||
* 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 "";
|
||||
var year = date.getFullYear().toString();
|
||||
var month = (date.getMonth() + 1).toString();
|
||||
var 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a QIDO URL given server details and a set of specified search filter
|
||||
* items
|
||||
*
|
||||
* @param server
|
||||
* @param filter
|
||||
* @returns {string} The URL with encoded filter query data
|
||||
*/
|
||||
function filterToQIDOURL(server, filter) {
|
||||
var commaSeparatedFields = [
|
||||
'00081030', // Study Description
|
||||
'00080060' //Modality
|
||||
// Add more fields here if you want them in the Study List
|
||||
].join(',');
|
||||
|
||||
var parameters = {
|
||||
PatientName: filter.patientName,
|
||||
PatientID: filter.patientId,
|
||||
AccessionNumber: filter.accessionNumber,
|
||||
StudyDescription: filter.studyDescription,
|
||||
ModalitiesInStudy: filter.modalitiesInStudy,
|
||||
limit: filter.limit,
|
||||
includefield: server.qidoSupportsIncludeField ? 'all' : commaSeparatedFields
|
||||
};
|
||||
|
||||
// build the StudyDate range parameter
|
||||
if (filter.studyDateFrom || filter.studyDateTo) {
|
||||
var date = "".concat(dateToString(new Date(filter.studyDateFrom)), "-", dateToString(new Date(filter.studyDateTo)));
|
||||
parameters.StudyDate = date;
|
||||
}
|
||||
|
||||
return server.qidoRoot + '/studies?' + encodeQueryData(parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses resulting data from a QIDO call into a set of Study MetaData
|
||||
*
|
||||
* @param resultData
|
||||
* @returns {Array} An array of Study MetaData objects
|
||||
*/
|
||||
function resultDataToStudies(resultData) {
|
||||
var studies = [];
|
||||
|
||||
if (!resultData || !resultData.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
resultData.forEach(function(study) {
|
||||
studies.push({
|
||||
studyInstanceUid: DICOMWeb.getString(study['0020000D']),
|
||||
// 00080005 = SpecificCharacterSet
|
||||
studyDate: DICOMWeb.getString(study['00080020']),
|
||||
studyTime: DICOMWeb.getString(study['00080030']),
|
||||
accessionNumber: DICOMWeb.getString(study['00080050']),
|
||||
referringPhysicianName: DICOMWeb.getString(study['00080090']),
|
||||
// 00081190 = URL
|
||||
patientName: DICOMWeb.getName(study['00100010']),
|
||||
patientId: DICOMWeb.getString(study['00100020']),
|
||||
patientBirthdate: DICOMWeb.getString(study['00100030']),
|
||||
patientSex: DICOMWeb.getString(study['00100040']),
|
||||
studyId: DICOMWeb.getString(study['00200010']),
|
||||
numberOfStudyRelatedSeries: DICOMWeb.getString(study['00201206']),
|
||||
numberOfStudyRelatedInstances: DICOMWeb.getString(study['00201208']),
|
||||
studyDescription: DICOMWeb.getString(study['00081030']),
|
||||
// modality: DICOMWeb.getString(study['00080060']),
|
||||
// modalitiesInStudy: DICOMWeb.getString(study['00080061']),
|
||||
modalities: DICOMWeb.getString(DICOMWeb.getModalities(study['00080060'], study['00080061']))
|
||||
});
|
||||
});
|
||||
|
||||
return studies;
|
||||
}
|
||||
|
||||
Services.QIDO.Studies = function(server, filter) {
|
||||
var url = filterToQIDOURL(server, filter);
|
||||
|
||||
try {
|
||||
var result = DICOMWeb.getJSON(url, server.requestOptions);
|
||||
|
||||
return resultDataToStudies(result.data);
|
||||
} catch (error) {
|
||||
OHIF.log.trace();
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -1,84 +0,0 @@
|
||||
import { remoteGetValue } from '../../lib/remoteGetValue';
|
||||
|
||||
/**
|
||||
* Parses data returned from a QIDO search and transforms it into
|
||||
* an array of series that are present in the study
|
||||
*
|
||||
* @param server The DICOM server
|
||||
* @param studyInstanceUid
|
||||
* @param resultData
|
||||
* @returns {Array} Series List
|
||||
*/
|
||||
function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
|
||||
resultData.forEach(function(instance) {
|
||||
// Use seriesMap to cache series data
|
||||
// If the series instance UID has already been used to
|
||||
// process series data, continue using that series
|
||||
var seriesInstanceUid = remoteGetValue(instance['0020,000e']);
|
||||
var series = seriesMap[seriesInstanceUid];
|
||||
|
||||
// If no series data exists in the seriesMap cache variable,
|
||||
// process any available series data
|
||||
if(!series) {
|
||||
series = {
|
||||
seriesInstanceUid : seriesInstanceUid,
|
||||
seriesNumber : parseFloat(remoteGetValue(instance['0020,0011'])),
|
||||
instances: []
|
||||
};
|
||||
|
||||
// Save this data in the seriesMap cache variable
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
// The uri for the dicomweb
|
||||
// NOTE: DCM4CHEE seems to return the data zipped
|
||||
// NOTE: Orthanc returns the data with multi-part mime which cornerstoneWADOImageLoader doesn't
|
||||
// know how to parse yet
|
||||
//var uri = DICOMWeb.getString(instance['00081190']);
|
||||
//uri = uri.replace('wado-rs', 'dicom-web');
|
||||
|
||||
// manually create a WADO-URI from the UIDs
|
||||
// NOTE: Haven't been able to get Orthanc's WADO-URI to work yet - maybe its not configured?
|
||||
var sopInstanceUid = remoteGetValue(instance['0008,0018']);
|
||||
var uri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom";
|
||||
|
||||
// Add this instance to the current series
|
||||
series.instances.push({
|
||||
sopClassUid: remoteGetValue(instance['0008,0016']),
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
uri: uri,
|
||||
instanceNumber: parseFloat(remoteGetValue(instance['0020,0013']))
|
||||
});
|
||||
});
|
||||
return seriesList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a set of instances using a QIDO call
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {{wadoUriRoot: String, studyInstanceUid: String, seriesList: Array}}
|
||||
*/
|
||||
Services.REMOTE.Instances = function(server, studyInstanceUid) {
|
||||
var parameters = {
|
||||
PatientName: "",
|
||||
PatientID: "",
|
||||
AccessionNumber: "",
|
||||
SeriesInstanceUID: "",
|
||||
SeriesNumber : "",
|
||||
SOPClassUID : "",
|
||||
InstanceNumber : ""
|
||||
};
|
||||
|
||||
var remote = new OrthancRemote(server.root, server.sourceAE);
|
||||
|
||||
return {
|
||||
wadoUriRoot: server.wadoUriRoot,
|
||||
studyInstanceUid: studyInstanceUid,
|
||||
seriesList: resultDataToStudyMetadata(server, studyInstanceUid, remote.findInstances(server.modality, studyInstanceUid, null, parameters))
|
||||
};
|
||||
};
|
||||
@@ -1,141 +0,0 @@
|
||||
import { remoteGetValue } from '../../lib/remoteGetValue';
|
||||
import { parseFloatArray } from '../../lib/parseFloatArray';
|
||||
|
||||
/**
|
||||
* 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 = remoteGetValue(instance['0008,2112']);
|
||||
if (SourceImageSequence && SourceImageSequence.Value && SourceImageSequence.Value.length) {
|
||||
return SourceImageSequence.Value[0]['0008,1155'].Value[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses result data from a WADO search into Study MetaData
|
||||
* Returns an object populated with study metadata, including the
|
||||
* series list.
|
||||
*
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @param resultData
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
|
||||
if (!resultData.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var anInstance = resultData[0];
|
||||
if (!anInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
var studyData = {
|
||||
seriesList: seriesList,
|
||||
patientName: remoteGetValue(anInstance['0010,0010']),
|
||||
patientId: remoteGetValue(anInstance['0010,0020']),
|
||||
accessionNumber: remoteGetValue(anInstance['0008,0050']),
|
||||
studyDate: remoteGetValue(anInstance['0008,0020']),
|
||||
modalities: remoteGetValue(anInstance['0008,0061']),
|
||||
studyDescription: remoteGetValue(anInstance['0008,1030']),
|
||||
imageCount: remoteGetValue(anInstance['0020,1208']),
|
||||
studyInstanceUid: remoteGetValue(anInstance['0020,000d'])
|
||||
};
|
||||
|
||||
resultData.forEach(function(instance) {
|
||||
var seriesInstanceUid = remoteGetValue(instance['0020,000e']);
|
||||
var series = seriesMap[seriesInstanceUid];
|
||||
if (!series) {
|
||||
series = {
|
||||
seriesDescription: remoteGetValue(instance['0008,103e']),
|
||||
modality: remoteGetValue(instance['0008,0060']),
|
||||
seriesInstanceUid: seriesInstanceUid,
|
||||
seriesNumber: parseFloat(remoteGetValue(instance['0020,0011'])),
|
||||
instances: []
|
||||
};
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
var sopInstanceUid = remoteGetValue(instance['0008,0018']);
|
||||
|
||||
var instanceSummary = {
|
||||
imageType: remoteGetValue(instance['0008,0008']),
|
||||
sopClassUid: remoteGetValue(instance['0008,0016']),
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
instanceNumber: parseFloat(remoteGetValue(instance['0020,0013'])),
|
||||
imagePositionPatient: remoteGetValue(instance['0020,0032']),
|
||||
imageOrientationPatient: remoteGetValue(instance['0020,0037']),
|
||||
frameOfReferenceUID: remoteGetValue(instance['0020,0052']),
|
||||
sliceLocation: parseFloat(remoteGetValue(instance['0020,1041'])),
|
||||
samplesPerPixel: parseFloat(remoteGetValue(instance['0028,0002'])),
|
||||
photometricInterpretation: remoteGetValue(instance['0028,0004']),
|
||||
rows: parseFloat(remoteGetValue(instance['0028,0010'])),
|
||||
columns: parseFloat(remoteGetValue(instance['0028,0011'])),
|
||||
pixelSpacing: remoteGetValue(instance['0028,0030']),
|
||||
bitsAllocated: parseFloat(remoteGetValue(instance['0028,0100'])),
|
||||
bitsStored: parseFloat(remoteGetValue(instance['0028,0101'])),
|
||||
highBit: parseFloat(remoteGetValue(instance['0028,0102'])),
|
||||
pixelRepresentation: parseFloat(remoteGetValue(instance['0028,0103'])),
|
||||
windowCenter: remoteGetValue(instance['0028,1050']),
|
||||
windowWidth: remoteGetValue(instance['0028,1051']),
|
||||
rescaleIntercept: parseFloat(remoteGetValue(instance['0028,1052'])),
|
||||
rescaleSlope: parseFloat(remoteGetValue(instance['0028,1053'])),
|
||||
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
|
||||
laterality: remoteGetValue(instance['0020,0062']),
|
||||
viewPosition: remoteGetValue(instance['0018,5101']),
|
||||
acquisitionDateTime: remoteGetValue(instance['0008,002A']),
|
||||
numberOfFrames: parseFloat(remoteGetValue(instance['0028,0008'])),
|
||||
frameIncrementPointer: remoteGetValue(instance['0028,0009']),
|
||||
frameTime: parseFloat(remoteGetValue(instance['0018,1063'])),
|
||||
frameTimeVector: parseFloatArray(remoteGetValue(instance['0018,1065'])),
|
||||
echoNumber: remoteGetValue(instance['0018,0086']),
|
||||
contrastBolusAgent: remoteGetValue(instance['0018,0010'])
|
||||
};
|
||||
|
||||
var iid = instance['xxxx,0001'].Value;
|
||||
if (server.imageRendering === 'wadouri') {
|
||||
instanceSummary.wadouri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom";
|
||||
} else if (server.imageRendering == 'orthanc') {
|
||||
instanceSummary.wadouri = server.root + '/instances/' + iid + '/file';
|
||||
} else {
|
||||
instanceSummary.wadorsuri = server.wadoRoot + '/studies/' + studyInstanceUid + '/series/' + seriesInstanceUid + '/instances/' + sopInstanceUid + '/frames/1';
|
||||
}
|
||||
|
||||
series.instances.push(instanceSummary);
|
||||
});
|
||||
console.log(studyData.seriesList[0].instances);
|
||||
return studyData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieved Study MetaData from a DICOM server using a WADO call
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
Services.REMOTE.RetrieveMetadata = function(server, studyInstanceUid) {
|
||||
var remote = new OrthancRemote(server.root, server.sourceAE);
|
||||
|
||||
var study = resultDataToStudyMetadata(server, studyInstanceUid, remote.retrieveMetadata(server.modality, studyInstanceUid));
|
||||
if (!study) {
|
||||
study = {};
|
||||
}
|
||||
|
||||
study.wadoUriRoot = server.wadoUriRoot;
|
||||
study.studyInstanceUid = studyInstanceUid;
|
||||
|
||||
return study;
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
import { remoteGetValue } from '../../lib/remoteGetValue';
|
||||
|
||||
function resultDataToStudies(resultData) {
|
||||
var studies = [];
|
||||
|
||||
resultData.forEach(function(study) {
|
||||
studies.push({
|
||||
studyInstanceUid: remoteGetValue(study['0020,000d']),
|
||||
// 00080005 = SpecificCharacterSet
|
||||
studyDate: remoteGetValue(study['0008,0020']),
|
||||
studyTime: remoteGetValue(study['0008,0030']),
|
||||
accessionNumber: remoteGetValue(study['0008,0050']),
|
||||
referringPhysicianName: remoteGetValue(study['0008,0090']),
|
||||
// 00081190 = URL
|
||||
patientName: remoteGetValue(study['0010,0010']),
|
||||
patientId: remoteGetValue(study['0010,0020']),
|
||||
patientBirthdate: remoteGetValue(study['0010,0030']),
|
||||
patientSex: remoteGetValue(study['0010,0040']),
|
||||
studyId: remoteGetValue(study['0020,0010']),
|
||||
numberOfStudyRelatedSeries: parseFloat(remoteGetValue(study['0020,1206'])),
|
||||
numberOfStudyRelatedInstances: parseFloat(remoteGetValue(study['0020,1208'])),
|
||||
studyDescription: remoteGetValue(study['0008,1030']),
|
||||
modalities: remoteGetValue(study['0008,0061'])
|
||||
});
|
||||
});
|
||||
return studies;
|
||||
}
|
||||
|
||||
Services.REMOTE.Studies = function(server, filter) {
|
||||
var parameters = {
|
||||
PatientName: filter.patientName ? filter.patientName : "",
|
||||
PatientID: filter.patientId,
|
||||
AccessionNumber: filter.accessionNumber ? filter.accessionNumber : "",
|
||||
StudyDescription: "",
|
||||
StudyDate : "",
|
||||
StudyTime : "",
|
||||
ReferringPhysicianName : "",
|
||||
PatientBirthDate : "",
|
||||
PatientSex : "",
|
||||
StudyID : "",
|
||||
NumberOfStudyRelatedSeries : "",
|
||||
NumberOfStudyRelatedInstances : "",
|
||||
ModalitiesInStudy : ""
|
||||
};
|
||||
var remote = new OrthancRemote(server.root, server.sourceAE);
|
||||
var data = remote.findStudies(server.modality, parameters);
|
||||
|
||||
return resultDataToStudies(data.results);
|
||||
};
|
||||
@@ -1,360 +0,0 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { parseFloatArray } from '../../lib/parseFloatArray';
|
||||
|
||||
/**
|
||||
* 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...
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a URL for a WADO search
|
||||
*
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {string}
|
||||
*/
|
||||
function buildUrl(server, studyInstanceUid) {
|
||||
return server.wadoRoot + '/studies/' + studyInstanceUid + '/metadata';
|
||||
}
|
||||
|
||||
/** Returns a WADO url for an instance
|
||||
*
|
||||
* @param studyInstanceUid
|
||||
* @param seriesInstanceUid
|
||||
* @param sopInstanceUid
|
||||
* @returns {string}
|
||||
*/
|
||||
function buildInstanceWadoUrl(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid) {
|
||||
const params = [];
|
||||
|
||||
params.push('requestType=WADO');
|
||||
params.push(`studyUID=${studyInstanceUid}`);
|
||||
params.push(`seriesUID=${seriesInstanceUid}`);
|
||||
params.push(`objectUID=${sopInstanceUid}`);
|
||||
params.push('contentType=application%2Fdicom');
|
||||
params.push('transferSyntax=*');
|
||||
|
||||
return `${server.wadoUriRoot}?${params.join('&')}`;
|
||||
}
|
||||
|
||||
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}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return SourceImageSequence.Value[0]['00081155'].Value[0];
|
||||
}
|
||||
}
|
||||
|
||||
function getPaletteColor(server, instance, tag, lutDescriptor) {
|
||||
const lut = [];
|
||||
const numLutEntries = lutDescriptor[0];
|
||||
const bits = lutDescriptor[2];
|
||||
const uri = WADOProxy.convertURL(instance[tag].BulkDataURI, server)
|
||||
const data = DICOMWeb.getBulkData(uri);
|
||||
|
||||
for (var i = 0; i < numLutEntries; i++) {
|
||||
if(bits === 16) {
|
||||
lut[i] = data.readUInt16LE(i*2);
|
||||
} else {
|
||||
lut[i] = data.readUInt8(i);
|
||||
}
|
||||
}
|
||||
|
||||
return lut;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
function getPaletteColors(server, instance, lutDescriptor) {
|
||||
|
||||
let entry = null,
|
||||
paletteUID = DICOMWeb.getString(instance['00281199']);
|
||||
|
||||
if (paletteColorCache.isValidUID(paletteUID)) {
|
||||
entry = paletteColorCache.get(paletteUID);
|
||||
} else {
|
||||
paletteUID = null;
|
||||
}
|
||||
|
||||
if (!entry) {
|
||||
// no entry on cache... Fetch remote data.
|
||||
try {
|
||||
let r, g, b;
|
||||
r = getPaletteColor(server, instance, '00281201', lutDescriptor);
|
||||
g = getPaletteColor(server, instance, '00281202', lutDescriptor);;
|
||||
b = getPaletteColor(server, instance, '00281203', lutDescriptor);;
|
||||
|
||||
entry = { red: r, green: g, blue: b };
|
||||
if (paletteUID !== null) {
|
||||
// when paletteUID is present, the entry can be cached...
|
||||
entry.uid = paletteUID;
|
||||
paletteColorCache.add(entry);
|
||||
}
|
||||
} catch (error) {
|
||||
OHIF.log.error(`(${error.name}) ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return entry;
|
||||
|
||||
}
|
||||
|
||||
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 result data from a WADO search into Study MetaData
|
||||
* Returns an object populated with study metadata, including the
|
||||
* series list.
|
||||
*
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @param resultData
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
|
||||
if (!resultData.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var anInstance = resultData[0];
|
||||
if (!anInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
var studyData = {
|
||||
seriesList: seriesList,
|
||||
patientName: DICOMWeb.getName(anInstance['00100010']),
|
||||
patientId: DICOMWeb.getString(anInstance['00100020']),
|
||||
patientAge: DICOMWeb.getNumber(anInstance['00101010']),
|
||||
patientSize: DICOMWeb.getNumber(anInstance['00101020']),
|
||||
patientWeight: DICOMWeb.getNumber(anInstance['00101030']),
|
||||
accessionNumber: DICOMWeb.getString(anInstance['00080050']),
|
||||
studyDate: DICOMWeb.getString(anInstance['00080020']),
|
||||
modalities: DICOMWeb.getString(anInstance['00080061']),
|
||||
studyDescription: DICOMWeb.getString(anInstance['00081030']),
|
||||
imageCount: DICOMWeb.getString(anInstance['00201208']),
|
||||
studyInstanceUid: DICOMWeb.getString(anInstance['0020000D']),
|
||||
institutionName: DICOMWeb.getString(anInstance['00080080'])
|
||||
};
|
||||
|
||||
resultData.forEach(function(instance) {
|
||||
var seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
|
||||
var series = seriesMap[seriesInstanceUid];
|
||||
if (!series) {
|
||||
series = {
|
||||
seriesDescription: DICOMWeb.getString(instance['0008103E']),
|
||||
modality: DICOMWeb.getString(instance['00080060']),
|
||||
seriesInstanceUid: seriesInstanceUid,
|
||||
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
|
||||
seriesDate: DICOMWeb.getString(instance['00080021']),
|
||||
seriesTime: DICOMWeb.getString(instance['00080031']),
|
||||
instances: []
|
||||
};
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
var 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);
|
||||
|
||||
var instanceSummary = {
|
||||
imageType: DICOMWeb.getString(instance['00080008']),
|
||||
sopClassUid: DICOMWeb.getString(instance['00080016']),
|
||||
modality: DICOMWeb.getString(instance['00080060']),
|
||||
sopInstanceUid: 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']),
|
||||
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),
|
||||
imageRendering: server.imageRendering,
|
||||
thumbnailRendering: server.thumbnailRendering
|
||||
};
|
||||
|
||||
// Get additional information if the instance uses "PALETTE COLOR" photometric interpretation
|
||||
if (instanceSummary.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 = getPaletteColors(server, instance, redPaletteColorLookupTableDescriptor);
|
||||
|
||||
if (palettes) {
|
||||
if (palettes.uid) {
|
||||
instanceSummary.paletteColorLookupTableUID = palettes.uid;
|
||||
}
|
||||
instanceSummary.redPaletteColorLookupTableData = palettes.red;
|
||||
instanceSummary.greenPaletteColorLookupTableData = palettes.green;
|
||||
instanceSummary.bluePaletteColorLookupTableData = palettes.blue;
|
||||
instanceSummary.redPaletteColorLookupTableDescriptor = redPaletteColorLookupTableDescriptor;
|
||||
instanceSummary.greenPaletteColorLookupTableDescriptor = greenPaletteColorLookupTableDescriptor;
|
||||
instanceSummary.bluePaletteColorLookupTableDescriptor = bluePaletteColorLookupTableDescriptor;
|
||||
}
|
||||
}
|
||||
|
||||
series.instances.push(instanceSummary);
|
||||
|
||||
});
|
||||
|
||||
return studyData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieved Study MetaData from a DICOM server using a WADO call
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
Services.WADO.RetrieveMetadata = function(server, studyInstanceUid) {
|
||||
var url = buildUrl(server, studyInstanceUid);
|
||||
|
||||
try {
|
||||
var result = DICOMWeb.getJSON(url, server.requestOptions);
|
||||
|
||||
var study = resultDataToStudyMetadata(server, studyInstanceUid, result.data);
|
||||
if (!study) {
|
||||
study = {};
|
||||
}
|
||||
|
||||
study.wadoUriRoot = server.wadoUriRoot;
|
||||
study.studyInstanceUid = studyInstanceUid;
|
||||
|
||||
return study;
|
||||
} catch (error) {
|
||||
OHIF.log.trace();
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user