First pass at a Hanging Protocol implementation and editor (LT-131)
This commit is contained in:
1 parent
dfcb33153f
commit
2e52d9e3c1
157 files changed
+8869
-23585
No files matched your search
@@ -0,0 +1,17 @@
|
||||
<template name="progressDialog">
|
||||
<div id="progressDialog" tabindex="0">
|
||||
<div class="dialogHeader">
|
||||
<h5>{{progressDialogTitle}}</h5>
|
||||
</div>
|
||||
<div class="dialogContent">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-striped active" role="progressbar" style="width:{{progressStatus}}">
|
||||
{{progressStatus}}
|
||||
</div>
|
||||
</div>
|
||||
<div id="message">
|
||||
{{{progressMessage}}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,56 @@
|
||||
progressDialog = {
|
||||
'show': function(title, numberOfTotal) {
|
||||
Session.set("progressDialogSettings", { title: title, numberOfCompleted: 0, numberOfTotal: numberOfTotal });
|
||||
|
||||
$('#progressDialog').css('display', 'block');
|
||||
},
|
||||
'update': function(numberOfCompleted) {
|
||||
var progressDialogSettings = Session.get("progressDialogSettings");
|
||||
progressDialogSettings.numberOfCompleted = numberOfCompleted;
|
||||
|
||||
Session.set("progressDialogSettings", progressDialogSettings);
|
||||
|
||||
if (progressDialogSettings.numberOfCompleted === progressDialogSettings.numberOfTotal) {
|
||||
progressDialog.close();
|
||||
}
|
||||
},
|
||||
'close': function() {
|
||||
Session.set("progressDialogSettings", { title: "", numberOfCompleted: 0, numberOfTotal: 1 });
|
||||
$('#progressDialog').css('display', 'none');
|
||||
},
|
||||
'setMessage': function(message) {
|
||||
var progressDialogSettings = Session.get("progressDialogSettings");
|
||||
progressDialogSettings.message = message;
|
||||
Session.set("progressDialogSettings", progressDialogSettings);
|
||||
}
|
||||
};
|
||||
|
||||
Template.progressDialog.helpers({
|
||||
'progressDialogTitle': function () {
|
||||
if (Session.get("progressDialogSettings") && Session.get("progressDialogSettings").title) {
|
||||
return Session.get("progressDialogSettings").title;
|
||||
}
|
||||
|
||||
return "";
|
||||
},
|
||||
'progressStatus': function() {
|
||||
var numberOfCompleted = 0;
|
||||
if (Session.get("progressDialogSettings") && Session.get("progressDialogSettings").numberOfCompleted) {
|
||||
numberOfCompleted = Session.get("progressDialogSettings").numberOfCompleted;
|
||||
}
|
||||
|
||||
var numberofTotal = 1;
|
||||
if (Session.get("progressDialogSettings") && Session.get("progressDialogSettings").numberOfTotal) {
|
||||
numberofTotal = Session.get("progressDialogSettings").numberOfTotal;
|
||||
}
|
||||
|
||||
return parseInt((numberOfCompleted / numberofTotal) * 100) + "%";
|
||||
},
|
||||
'progressMessage': function() {
|
||||
var progressDialogSettings = Session.get("progressDialogSettings");
|
||||
if (progressDialogSettings && progressDialogSettings.message) {
|
||||
return progressDialogSettings.message;
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
#progressDialog
|
||||
display: none
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
right: 0
|
||||
margin: auto
|
||||
width: 20%
|
||||
height: 20%
|
||||
z-index: 100
|
||||
border-radius: 5px
|
||||
padding: 10px 20px 10px 20px
|
||||
background-color: rgba(255,255,255,1)
|
||||
outline: none
|
||||
|
||||
.dialogContent
|
||||
margin-bottom: 20px
|
||||
|
||||
#message
|
||||
width: 100%
|
||||
|
||||
span
|
||||
display: inline-block;
|
||||
width: 100%
|
||||
@@ -20,10 +20,20 @@ WorklistStudies = new Meteor.Collection(null);
|
||||
WorklistStudies._debugName = 'WorklistStudies';
|
||||
|
||||
Template.worklist.onRendered(function() {
|
||||
// If there is a tab set as active in the Session,
|
||||
// switch to that now.
|
||||
var contentId = Session.get('activeContentId');
|
||||
switchToTab(contentId);
|
||||
var templateData = Template.currentData();
|
||||
if (templateData && templateData.studyInstanceUid) {
|
||||
var studyInstanceUid = templateData.studyInstanceUid;
|
||||
openNewTab(studyInstanceUid, studyInstanceUid);
|
||||
} else {
|
||||
// If there is a tab set as active in the Session,
|
||||
// switch to that now.
|
||||
var contentId = Session.get('activeContentId');
|
||||
|
||||
// TODO: FIx this it seems to be forcing two switches
|
||||
switchToTab(contentId);
|
||||
}
|
||||
|
||||
Meteor.subscribe('hangingprotocols');
|
||||
});
|
||||
|
||||
Template.worklist.helpers({
|
||||
|
||||
@@ -97,5 +97,4 @@
|
||||
{{>studyNotFound}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
</template>
|
||||
@@ -164,7 +164,7 @@ function search() {
|
||||
});
|
||||
|
||||
if (WorklistStudies.find().count() === 0) {
|
||||
// Show notFound text
|
||||
// Show studyNotFound text
|
||||
searchResults.showNotFoundMessage = true;
|
||||
Session.set('searchResults', searchResults);
|
||||
}
|
||||
|
||||
@@ -104,4 +104,4 @@
|
||||
border-top: 0.5px dashed #dddddd
|
||||
border-bottom: 0.5px dashed #dddddd
|
||||
color: white
|
||||
background-color: #009BD2
|
||||
background-color: #009BD2
|
||||
@@ -0,0 +1,115 @@
|
||||
var exportFailed;
|
||||
|
||||
/**
|
||||
* Exports requested studies
|
||||
* @param studiesToExport Studies to export
|
||||
*/
|
||||
exportStudies = function(studiesToExport) {
|
||||
if (studiesToExport.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
queryStudies(studiesToExport, exportQueriedStudies);
|
||||
};
|
||||
|
||||
function exportQueriedStudies(studiesToExport) {
|
||||
var numberOfFilesToExport = 0;
|
||||
studiesToExport.forEach(function(study) {
|
||||
numberOfFilesToExport += getNumberOfFilesInStudy(study);
|
||||
});
|
||||
|
||||
progressDialog.show("Exporting Studies...", numberOfFilesToExport);
|
||||
|
||||
try {
|
||||
exportQueriedStudiesInternal(studiesToExport, numberOfFilesToExport);
|
||||
} catch (err) {
|
||||
progressDialog.close();
|
||||
console.error("Failed to export studies: " + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function exportQueriedStudiesInternal(studiesToExport, numberOfFilesToExport) {
|
||||
var zip = new JSZip();
|
||||
|
||||
exportFailed = false;
|
||||
var numberOfFilesExported = 0;
|
||||
|
||||
studiesToExport.forEach(function(study) {
|
||||
sortStudy(study);
|
||||
|
||||
var studyFolder = zip.folder(study.studyInstanceUid);
|
||||
|
||||
study.seriesList.forEach(function(series) {
|
||||
var seriesFolder = studyFolder.folder(series.seriesInstanceUid);
|
||||
|
||||
series.instances.forEach(function(instance) {
|
||||
if (!instance.wadouri) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If failed to download a dicom file, skip others
|
||||
if (exportFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Download and Zip the dicom file
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", instance.wadouri, true);
|
||||
xhr.responseType = "blob";
|
||||
|
||||
// Downloaded the dicom file completely
|
||||
xhr.onload = function(e) {
|
||||
// If failed to download a dicom file, skip others
|
||||
if (exportFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Failed to export a file
|
||||
if (xhr.readyState === 4 && xhr.status !== 200) {
|
||||
onExportFailed("File not downloaded: " + instance.wadouri);
|
||||
return;
|
||||
}
|
||||
|
||||
var blobFile = new Blob([xhr.response], {type: 'application/dicom'});
|
||||
|
||||
var fileReader = new FileReader();
|
||||
|
||||
fileReader.onload = function() {
|
||||
try {
|
||||
seriesFolder.file(instance.sopInstanceUid + ".dcm", fileReader.result, { binary: true });
|
||||
} catch(err) {
|
||||
onExportFailed(err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
numberOfFilesExported++;
|
||||
|
||||
if (numberOfFilesExported === numberOfFilesToExport) {
|
||||
var zipContent = zip.generate({ type: "blob" });
|
||||
saveAs(zipContent, "studies.zip");
|
||||
}
|
||||
|
||||
progressDialog.update(numberOfFilesExported);
|
||||
};
|
||||
|
||||
fileReader.readAsArrayBuffer(blobFile);
|
||||
};
|
||||
|
||||
// Failed to download the dicom file
|
||||
xhr.onerror = function() {
|
||||
onExportFailed("File not downloaded: " + instance.wadouri);
|
||||
};
|
||||
|
||||
xhr.send();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onExportFailed(err) {
|
||||
exportFailed = true;
|
||||
progressDialog.close();
|
||||
|
||||
//TODO: Export failed and dialog closed, so let user know
|
||||
console.error("Failed to export studies!", err);
|
||||
}
|
||||
-4
@@ -22,10 +22,6 @@ getStudiesMetadata = function(studyInstanceUids, doneCallback, failCallback) {
|
||||
// retrieval call
|
||||
var promises = [];
|
||||
|
||||
// Create an empty array to hold all the results of the promises
|
||||
var studies = [];
|
||||
|
||||
|
||||
// Loop through the array of studyInstanceUids
|
||||
studyInstanceUids.forEach(function(studyInstanceUid) {
|
||||
// Create a new Deferred to monitor the progress of the asynchronous
|
||||
+4
-4
@@ -24,12 +24,12 @@ getStudyMetadata = function(studyInstanceUid, doneCallback, failCallback) {
|
||||
// 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) {
|
||||
if (Meteor.user()) {
|
||||
if (Meteor.user && Meteor.user()) {
|
||||
var hipaaEvent = {
|
||||
eventType: "viewed",
|
||||
eventType: 'viewed',
|
||||
userId: Meteor.userId(),
|
||||
userName: Meteor.user().profile.fullName,
|
||||
collectionName: "Study",
|
||||
collectionName: 'Study',
|
||||
recordId: studyInstanceUid,
|
||||
patientId: study.patientId,
|
||||
patientName: study.patientName
|
||||
@@ -55,4 +55,4 @@ getStudyMetadata = function(studyInstanceUid, doneCallback, failCallback) {
|
||||
// Finally, we fire the doneCallback with this study meta data
|
||||
doneCallback(study);
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -15,6 +15,7 @@ openNewTab = function(studyInstanceUid, title) {
|
||||
WorklistTabs.insert({
|
||||
title: title,
|
||||
contentid: contentid,
|
||||
studyInstanceUid: studyInstanceUid,
|
||||
active: false
|
||||
});
|
||||
|
||||
File renamed without changes.
File renamed without changes.
@@ -5,7 +5,7 @@ Package.describe({
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.versionsFrom('1.2.0.2');
|
||||
api.versionsFrom('1.2.1');
|
||||
|
||||
api.use('standard-app-packages');
|
||||
api.use('jquery');
|
||||
@@ -17,11 +17,13 @@ Package.onUse(function (api) {
|
||||
|
||||
// Our custom packages
|
||||
api.use('dicomweb');
|
||||
api.use('dimseservice');
|
||||
api.use('orthanc-remote');
|
||||
|
||||
// This sets the default logging level of the package using the
|
||||
// loglevel package. It can be overridden in the JavaScript
|
||||
// console for debugging purposes
|
||||
api.addFiles('log.js', 'client');
|
||||
api.addFiles('log.js');
|
||||
|
||||
// Components
|
||||
api.addFiles('client/components/worklist.html', 'client');
|
||||
@@ -51,12 +53,40 @@ Package.onUse(function (api) {
|
||||
api.addFiles('client/components/worklistToolbar/worklistToolbar.js', 'client');
|
||||
api.addFiles('client/components/worklistToolbar/worklistToolbar.styl', 'client');
|
||||
|
||||
// Library functions
|
||||
api.addFiles('lib/getStudyMetadata.js', 'client');
|
||||
api.addFiles('lib/getStudiesMetadata.js', 'client');
|
||||
api.addFiles('lib/openNewTab.js', 'client');
|
||||
api.addFiles('lib/switchToTab.js', 'client');
|
||||
api.addFiles('lib/worklist.js', 'client');
|
||||
api.addFiles('client/components/progressDialog/progressDialog.html', 'client');
|
||||
api.addFiles('client/components/progressDialog/progressDialog.styl', 'client');
|
||||
api.addFiles('client/components/progressDialog/progressDialog.js', 'client');
|
||||
|
||||
// Client-side library functions
|
||||
api.addFiles('client/lib/getStudyMetadata.js', 'client');
|
||||
api.addFiles('client/lib/getStudiesMetadata.js', 'client');
|
||||
api.addFiles('client/lib/openNewTab.js', 'client');
|
||||
api.addFiles('client/lib/switchToTab.js', 'client');
|
||||
api.addFiles('client/lib/exportStudies.js', 'client');
|
||||
api.addFiles('client/lib/worklist.js', 'client');
|
||||
|
||||
// Server-side functions
|
||||
api.addFiles('server/lib/namespace.js', 'server');
|
||||
api.addFiles('server/lib/encodeQueryData.js', 'server');
|
||||
api.addFiles('server/methods/getStudyMetadata.js', 'server');
|
||||
api.addFiles('server/methods/worklistSearch.js', 'server');
|
||||
|
||||
// DICOMWeb instance, study, and metadata retrieval
|
||||
api.addFiles('server/services/qido/instances.js', 'server');
|
||||
api.addFiles('server/services/qido/studies.js', 'server');
|
||||
api.addFiles('server/services/wado/retrieveMetadata.js', 'server');
|
||||
|
||||
// DIMSE instance, study, and metadata retrieval
|
||||
api.addFiles('server/services/dimse/instances.js', 'server');
|
||||
api.addFiles('server/services/dimse/studies.js', 'server');
|
||||
api.addFiles('server/services/dimse/retrieveMetadata.js', 'server');
|
||||
|
||||
// Study, instance, and metadata retrieval from remote PACS via Orthanc as a proxy
|
||||
api.addFiles('server/services/remote/instances.js', 'server');
|
||||
api.addFiles('server/services/remote/studies.js', 'server');
|
||||
api.addFiles('server/services/remote/retrieveMetadata.js', 'server');
|
||||
|
||||
api.export('Services', 'server');
|
||||
|
||||
// Export Worklist helper functions for usage in Routes
|
||||
api.export('getTimepointName', 'client');
|
||||
@@ -65,6 +95,7 @@ Package.onUse(function (api) {
|
||||
api.export('openNewTab', 'client');
|
||||
api.export('setWorklistSubscriptions', 'client');
|
||||
api.export('switchToTab', 'client');
|
||||
api.export('progressDialog', 'client');
|
||||
api.export('Worklist');
|
||||
|
||||
// Export the global ViewerData object
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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('&');
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
Services = {};
|
||||
Services.QIDO = {};
|
||||
Services.WADO = {};
|
||||
Services.DIMSE = {};
|
||||
Services.REMOTE = {};
|
||||
|
||||
remoteGetValue = function(obj) {
|
||||
return obj ? obj.Value : null;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
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) {
|
||||
log.info('GetStudyMetadata(%s)', studyInstanceUid);
|
||||
|
||||
if (!Meteor.settings.dicomWeb) {
|
||||
throw 'No properly configured server was available over DICOMWeb';
|
||||
}
|
||||
|
||||
if (Meteor.settings.dicomWeb && Meteor.settings.defaultServiceType === 'dicomWeb') {
|
||||
// Get the server data. This is user-defined in the
|
||||
// config.json files used to run the Meteor server
|
||||
var server = Meteor.settings.dicomWeb.endpoints[0];
|
||||
return Services.WADO.RetrieveMetadata(server, studyInstanceUid);
|
||||
} else if (Meteor.settings.dimse && Meteor.settings.defaultServiceType === 'dimse') {
|
||||
return Services.DIMSE.RetrieveMetadata(studyInstanceUid);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
Meteor.methods({
|
||||
/**
|
||||
* Use the specified filter to conduct a search from the DICOM server
|
||||
* @param filter
|
||||
*/
|
||||
WorklistSearch: function(filter) {
|
||||
if (Meteor.settings.dicomWeb && Meteor.settings.defaultServiceType === 'dicomWeb') {
|
||||
// Get the server data. This is user-defined in the
|
||||
// config.json files used to run the Meteor server
|
||||
var server = Meteor.settings.dicomWeb.endpoints[0];
|
||||
|
||||
return Services.QIDO.Studies(server, filter);
|
||||
} else if (Meteor.settings.dimse && Meteor.settings.defaultServiceType === 'dimse') {
|
||||
return Services.DIMSE.Studies(filter);
|
||||
} else {
|
||||
throw 'No properly configured server was available over DICOMWeb or DIMSE.';
|
||||
}
|
||||
}
|
||||
});
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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) {
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
|
||||
resultData.forEach(function(instanceRaw) {
|
||||
var 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
|
||||
var seriesInstanceUid = instance[0x0020000E];
|
||||
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: instance[0x00200011],
|
||||
instances: []
|
||||
};
|
||||
|
||||
// Save this data in the seriesMap cache variable
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
var host = Meteor.settings.dimse.host,
|
||||
port = Meteor.settings.dimse.port;
|
||||
|
||||
var serverRoot = host + ':' + port;
|
||||
|
||||
var sopInstanceUid = instance[0x00080018];
|
||||
var uri = serverRoot + '/studies/' + studyInstanceUid + '/series/' + seriesInstanceUid + '/instances/' + sopInstanceUid + '/frames/1';
|
||||
|
||||
// Add this instance to the current series
|
||||
series.instances.push({
|
||||
sopClassUid: instance[0x00080016],
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
uri: 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);
|
||||
var result = DIMSE.retrieveInstances(studyInstanceUid);
|
||||
|
||||
return {
|
||||
studyInstanceUid: studyInstanceUid,
|
||||
seriesList: resultDataToStudyMetadata(result)
|
||||
};
|
||||
};
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* 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[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 resultData
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
function resultDataToStudyMetadata(studyInstanceUid, resultData) {
|
||||
log.info('resultDataToStudyMetadata');
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
|
||||
if (!resultData.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var anInstance = resultData[0].toObject();
|
||||
if (!anInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
var studyData = {
|
||||
seriesList: seriesList,
|
||||
patientName: anInstance[0x00100010],
|
||||
patientId: anInstance[0x00100020],
|
||||
accessionNumber: anInstance[0x00080050],
|
||||
studyDate: anInstance[0x00080020],
|
||||
modalities: anInstance[0x00080061],
|
||||
studyDescription: anInstance[0x00081030],
|
||||
imageCount: anInstance[0x00201208],
|
||||
studyInstanceUid: anInstance[0x0020000D]
|
||||
};
|
||||
|
||||
resultData.forEach(function(instanceRaw) {
|
||||
var instance = instanceRaw.toObject();
|
||||
var seriesInstanceUid = instance[0x0020000E];
|
||||
var 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);
|
||||
}
|
||||
|
||||
var sopInstanceUid = instance[0x00080018];
|
||||
|
||||
var instanceSummary = {
|
||||
imageType: instance[0x00080008],
|
||||
sopClassUid: instance[0x00080016],
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
instanceNumber: parseFloat(instance[0x00200013]),
|
||||
imagePositionPatient: instance[0x00200032],
|
||||
imageOrientationPatient: instance[0x00200037],
|
||||
frameOfReferenceUID: instance[0x00200052],
|
||||
sliceLocation: parseFloat(instance[0x00201041]),
|
||||
samplesPerPixel: parseFloat(instance[0x00280002]),
|
||||
photometricInterpretation: instance[0x00280004],
|
||||
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],
|
||||
numFrames: parseFloat(instance[0x00280008]),
|
||||
frameTime: parseFloat(instance[0x00181063])
|
||||
};
|
||||
|
||||
// Retrieve the actual data over WADO-URI
|
||||
var server = Meteor.settings.dicomWeb.endpoints[0];
|
||||
instanceSummary.wadouri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom";
|
||||
|
||||
series.instances.push(instanceSummary);
|
||||
});
|
||||
|
||||
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) {
|
||||
var result = DIMSE.retrieveInstances(studyInstanceUid);
|
||||
|
||||
var study = resultDataToStudyMetadata(studyInstanceUid, result);
|
||||
if (!study) {
|
||||
study = {};
|
||||
}
|
||||
|
||||
study.studyInstanceUid = studyInstanceUid;
|
||||
return study;
|
||||
};
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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) {
|
||||
log.info('Services.DIMSE.Studies');
|
||||
var parameters = {
|
||||
0x00100010: filter.patientName,
|
||||
0x00100020: filter.patientId,
|
||||
0x00080050: filter.accessionNumber,
|
||||
0x00081030: filter.studyDescription,
|
||||
0x00100040: '',
|
||||
0x00201208: '',
|
||||
0x00080061: ''
|
||||
};
|
||||
|
||||
var results = DIMSE.retrieveStudies(parameters);
|
||||
return resultDataToStudies(results);
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 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
|
||||
* @returns {{wadoUriRoot: String, studyInstanceUid: String, seriesList: Array}}
|
||||
*/
|
||||
Services.QIDO.Instances = function(server, studyInstanceUid) {
|
||||
var url = buildUrl(server, studyInstanceUid);
|
||||
var result = DICOMWeb.getJSON(url, server.requestOptions);
|
||||
|
||||
return {
|
||||
wadoUriRoot: server.wadoUriRoot,
|
||||
studyInstanceUid: studyInstanceUid,
|
||||
seriesList: resultDataToStudyMetadata(server, studyInstanceUid, result.data)
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 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,
|
||||
limit: filter.limit || 20,
|
||||
includefield: server.qidoSupportsIncludeField ? 'all' : commaSeparatedFields
|
||||
};
|
||||
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 = [];
|
||||
|
||||
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.getNumber(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);
|
||||
var result = DICOMWeb.getJSON(url, server.requestOptions);
|
||||
return resultDataToStudies(result.data);
|
||||
};
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 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))
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 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']),
|
||||
numFrames: parseFloat(remoteGetValue(instance['0028,0008'])),
|
||||
frameTime: parseFloat(remoteGetValue(instance['0018,1063']))
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
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);
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Creates a URL for a WADO search
|
||||
*
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {string}
|
||||
*/
|
||||
function buildUrl(server, studyInstanceUid) {
|
||||
return server.wadoRoot + '/studies/' + studyInstanceUid + '/metadata';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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']),
|
||||
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'])
|
||||
};
|
||||
|
||||
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']),
|
||||
instances: []
|
||||
};
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
var sopInstanceUid = DICOMWeb.getString(instance['00080018']);
|
||||
|
||||
var instanceSummary = {
|
||||
imageType: DICOMWeb.getString(instance['00080008']),
|
||||
sopClassUid: DICOMWeb.getString(instance['00080016']),
|
||||
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']),
|
||||
rows: DICOMWeb.getNumber(instance['00280010']),
|
||||
columns: DICOMWeb.getNumber(instance['00280011']),
|
||||
pixelSpacing: DICOMWeb.getString(instance['00280030']),
|
||||
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
|
||||
bitsStored: DICOMWeb.getNumber(instance['00280101']),
|
||||
highBit: DICOMWeb.getNumber(instance['00280102']),
|
||||
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
|
||||
windowCenter: DICOMWeb.getString(instance['00281050']),
|
||||
windowWidth: DICOMWeb.getString(instance['00281051']),
|
||||
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
|
||||
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
|
||||
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
|
||||
laterality: DICOMWeb.getString(instance['00200062']),
|
||||
viewPosition: DICOMWeb.getString(instance['00185101']),
|
||||
numFrames: DICOMWeb.getNumber(instance['00280008']),
|
||||
frameTime: DICOMWeb.getNumber(instance['00181063']),
|
||||
sliceThickness: DICOMWeb.getNumber(instance['00180050'])
|
||||
};
|
||||
|
||||
if (server.imageRendering === 'wadouri') {
|
||||
instanceSummary.wadouri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + '&contentType=application%2Fdicom';
|
||||
} else {
|
||||
instanceSummary.wadorsuri = server.wadoRoot + '/studies/' + studyInstanceUid + '/series/' + seriesInstanceUid + '/instances/' + sopInstanceUid + '/frames/1';
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
};
|
||||
Reference in new issue
Block a user