First pass at a Hanging Protocol implementation and editor (LT-131)

This commit is contained in:
Erik Ziegler committed 2016-05-07 12:40:02 +02:00
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);
}
@@ -0,0 +1,58 @@
/**
* Retrieves metaData for multiple studies at once.
*
* This function calls getStudyMetadata several times, asynchronously,
* and waits for all of the results to be returned.
*
* @param studyInstanceUids The UIDs of the Studies to be retrieved
* @param doneCallback The callback function to be executed when the study retrieval has finished
* @param failCallback The callback function to be executed when the study retrieval has failed
*/
getStudiesMetadata = function(studyInstanceUids, 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
var promises = [];
// Loop through the array of studyInstanceUids
studyInstanceUids.forEach(function(studyInstanceUid) {
// Create a new Deferred to monitor the progress of the asynchronous
// metaData retrieval
var deferred = new $.Deferred();
// Send the call, and attach doneCallbacks and failCallbacks
// which can resolve or reject the related promise based on its outcome
getStudyMetadata(studyInstanceUid, function(study) {
deferred.resolve(study);
}, function(error) {
deferred.reject(error);
});
// Add the current promise to the array of promises
promises.push(deferred.promise());
});
// When all of the promises are complete, this callback runs
$.when.apply($, promises).done(function() {
// Convert the Arguments Array-like Object to an actual array
var studies = $.makeArray(arguments);
// Pass the studies array to the doneCallback, if one exists
if (doneCallback && typeof doneCallback === 'function') {
doneCallback(studies);
}
}).fail(function(error) {
log.warn(error);
if (failCallback && typeof failCallback === 'function') {
failCallback(error);
}
});
};
@@ -0,0 +1,58 @@
// Define the StudyMetaData object. This is used as a cache
// to store study meta data information to prevent unnecessary
// calls to the server
var StudyMetaData = {};
/**
* Retrieves study metadata using a server call, and fires a callback
* when completed.
*
* @params {string} studyInstanceUid The UID of the Study to be retrieved
* @params {function} doneCallback The callback function to be executed when the study retrieval has finished
* @param failCallback The callback function to be executed when the study retrieval has failed
*/
getStudyMetadata = function(studyInstanceUid, doneCallback, failCallback) {
// If the StudyMetaData cache already has data related to this
// studyInstanceUid, then we should fire the doneCallback with this data
// and stop here.
var study = StudyMetaData[studyInstanceUid];
if (study) {
doneCallback(study);
return;
}
// 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 && Meteor.user()) {
var hipaaEvent = {
eventType: 'viewed',
userId: Meteor.userId(),
userName: Meteor.user().profile.fullName,
collectionName: 'Study',
recordId: studyInstanceUid,
patientId: study.patientId,
patientName: study.patientName
};
HipaaLogger.logEvent(hipaaEvent);
}
log.info('worklistStudy getStudyMetadata: ' + studyInstanceUid);
if (error) {
log.warn(error);
failCallback(error);
return;
}
// Once we have retrieved the data, we sort the series' by series
// and instance number in ascending order
sortStudy(study);
// Then we store this data in the cache variable
StudyMetaData[studyInstanceUid] = study;
// Finally, we fire the doneCallback with this study meta data
doneCallback(study);
});
};
@@ -0,0 +1,31 @@
/**
* Opens a new tab in the tabbed worklist environment using
* a given study and new tab title.
*
* @param studyInstanceUid The UID of the Study to be opened
* @param title The title to be used for the tab heading
*/
openNewTab = function(studyInstanceUid, title) {
// Generate a unique ID to represent this tab
// We can't just use the Mongo entry ID because
// then it will change after hot-reloading.
var contentid = uuid.new();
// Create a new entry in the WorklistTabs Collection
WorklistTabs.insert({
title: title,
contentid: contentid,
studyInstanceUid: studyInstanceUid,
active: false
});
// Update the ViewerData global object
ViewerData[contentid] = {
title: title,
contentid: contentid,
studyInstanceUids: [studyInstanceUid]
};
// Switch to the new tab
switchToTab(contentid);
};
+118
View File
@@ -0,0 +1,118 @@
/**
* Switches to a new tab in the tabbed worklist container
* This function renders either the Worklist or the Viewer template with new data.
*
* @param contentId The unique ID of the tab to be switched to
*/
switchToTab = function(contentId) {
if (!contentId) {
return;
}
log.info('Switching to tab: ' + contentId);
// Use Bootstrap's Tab JavaScript to show the contents of the current tab
// Unless it is the worklist, it is currently an empty div
$('.tabTitle a[data-target="#' + contentId + '"]').tab('show');
// Remove any previous Viewers from the DOM
$('.viewerContainer').remove();
$('.worklistContainer').remove();
// Update the 'activeContentId' variable in Session
Session.set('activeContentId', contentId);
// If we are switching to the Worklist tab, reset any CSS styles
// that have been applied to prevent scrolling in the Viewer.
// Then stop here, since nothing needs to be re-rendered.
var container;
if (contentId === 'worklistTab') {
container = $('.tab-content').find('#worklistTab').get(0);
var worklistContainer = document.createElement('div');
worklistContainer.classList.add('worklistContainer');
container.appendChild(worklistContainer);
// Use Blaze to render the WorklistResult Template into the container
Blaze.render(Template.worklistResult, worklistContainer);
document.body.style.overflow = null;
document.body.style.height = null;
document.body.style.minWidth = null;
document.body.style.position = null;
return;
}
// Tab was closed at some point, stop here
if (!ViewerData[contentId]) {
return;
}
// Use the stored ViewerData global object to retrieve the studyInstanceUid
// related to this tab
var studyInstanceUids = ViewerData[contentId].studyInstanceUids;
// Attempt to retrieve the meta data (it might be cached)
getStudiesMetadata(studyInstanceUids, function(studies) {
// Tab closed while study data was being retrieved, stop here
if (!ViewerData[contentId]) {
log.warn('Tab closed while study data was being retrieved');
return;
}
// Once we have the study data, store it in a structure with
// any other saved data about this tab (e.g. layout structure)
var data = jQuery.extend({}, ViewerData[contentId]);
data.studies = studies;
data.contentId = contentId;
if (ViewerData[contentId].studies && ViewerData[contentId].studies.length) {
data.studies = ViewerData[contentId].studies;
}
// Add additional metadata to our study from the worklist
data.studies.forEach(function(study) {
var worklistStudy = WorklistStudies.findOne({
studyInstanceUid: study.studyInstanceUid
});
if (!worklistStudy) {
return;
}
$.extend(study, worklistStudy);
});
// Get tab content container given the contentId string
// If no such container exists, stop here because something is wrong
var container = $('.tab-content').find('#' + contentId).get(0);
if (!container) {
log.warn('No container present with the contentId: ' + contentId);
return;
}
// Remove the loading text template that is inside the tab container by default
var viewerContainer = document.createElement('div');
viewerContainer.classList.add('viewerContainer');
container.innerHTML = '';
container.appendChild(viewerContainer);
// Use Blaze to render the Viewer Template into the container
Blaze.renderWithData(Template.viewer, data, viewerContainer);
// Retrieve the DOM element of the viewer
var imageViewer = $('#viewer');
// If it is present in the DOM (it should be), then apply
// styles to prevent page scrolling and overscrolling on mobile devices
if (imageViewer) {
document.body.style.overflow = 'hidden';
document.body.style.height = '100%';
document.body.style.width = '100%';
document.body.style.minWidth = 0;
// Prevent overscroll on mobile devices
document.body.style.position = 'fixed';
}
});
};
+13
View File
@@ -0,0 +1,13 @@
Worklist = {
functions: {},
callbacks: {}
};
Worklist.callbacks.dblClickOnStudy = dblClickOnStudy;
Worklist.callbacks.middleClickOnStudy = dblClickOnStudy;
function dblClickOnStudy(data) {
// Use the formatPN template helper to clean up the patient name
var title = formatPN(data.patientName);
openNewTab(data.studyInstanceUid, title);
}