Move server-side functions from viewerbase to worklist. Split multi-frame and single-image modalities into separate display sets (OHIF-36)
This commit is contained in:
1 parent
9d8c039ffc
commit
fe32c40832
32 files changed
+231
-793
No files matched your search
@@ -0,0 +1 @@
|
||||
StudyImportStatus = new Meteor.Collection('studyImportStatus');
|
||||
@@ -14,6 +14,8 @@ body
|
||||
background-color: $primaryBackgroundColor
|
||||
margin: 0 auto
|
||||
color: white
|
||||
float: left;
|
||||
width: 100%;
|
||||
|
||||
.tab-content
|
||||
width: 100%
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Imports selected studies from local into worklist
|
||||
* @param filesToImport Files located in the client machine to import
|
||||
*/
|
||||
importStudies = function(filesToImport, importCallback) {
|
||||
if (filesToImport.length < 1) {
|
||||
return;
|
||||
}
|
||||
var fileUploadStatus = {
|
||||
numberOfFilesUploaded: 0,
|
||||
numberOfFilesFailed: 0
|
||||
};
|
||||
var numberOfFilesToUpload = filesToImport.length;
|
||||
var studiesToImport = [];
|
||||
progressDialog.show({
|
||||
title: "Uploading Files...",
|
||||
numberOfCompleted: 0,
|
||||
numberOfTotal: numberOfFilesToUpload
|
||||
});
|
||||
|
||||
// Upload files to the server
|
||||
filesToImport.forEach(function(fileToUpload) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', "/uploadFilesToImport", true);
|
||||
xhr.setRequestHeader("filename", fileToUpload.name);
|
||||
|
||||
xhr.onload = function() {
|
||||
// Failed to upload a file
|
||||
if (xhr.readyState === 4 && xhr.status !== 200) {
|
||||
updateFileUploadStatus(fileUploadStatus, false);
|
||||
return;
|
||||
}
|
||||
|
||||
studiesToImport.push(xhr.responseText);
|
||||
|
||||
updateFileUploadStatus(fileUploadStatus, true);
|
||||
|
||||
var numberOfFilesProcessedToUpload = fileUploadStatus.numberOfFilesUploaded + fileUploadStatus.numberOfFilesFailed;
|
||||
progressDialog.update(numberOfFilesProcessedToUpload);
|
||||
|
||||
if (numberOfFilesToUpload === numberOfFilesProcessedToUpload) {
|
||||
// The upload is completed, so import files
|
||||
importStudiesInternal(studiesToImport, importCallback);
|
||||
|
||||
if (fileUploadStatus.numberOfFilesFailed > 0) {
|
||||
//TODO: Some files failed to upload, so let user know
|
||||
log.info("Failed to upload " + fileUploadStatus.numberOfFilesFailed + " of " + numberOfFilesToUpload + " files");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Failed to upload a file
|
||||
xhr.onerror = function() {
|
||||
updateFileUploadStatus(fileUploadStatus, false);
|
||||
};
|
||||
|
||||
xhr.send(fileToUpload);
|
||||
});
|
||||
};
|
||||
|
||||
function updateFileUploadStatus(fileUploadStatus, isSuccess) {
|
||||
if (!isSuccess) {
|
||||
fileUploadStatus.numberOfFilesFailed++;
|
||||
} else {
|
||||
fileUploadStatus.numberOfFilesUploaded++;
|
||||
}
|
||||
}
|
||||
function importStudiesInternal(studiesToImport, importCallback) {
|
||||
if (!studiesToImport) {
|
||||
return;
|
||||
}
|
||||
|
||||
var numberOfStudiesToImport = studiesToImport.length;
|
||||
|
||||
progressDialog.show({
|
||||
title: "Importing Studies...",
|
||||
numberOfCompleted: 0,
|
||||
numberOfTotal: numberOfStudiesToImport
|
||||
});
|
||||
|
||||
// Create/Insert a new study import status item
|
||||
Meteor.call("createStudyImportStatus", function(err, studyImportStatusId) {
|
||||
if (err) {
|
||||
// Hide dialog
|
||||
progressDialog.close();
|
||||
console.log(err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle when StudyImportStatus collection is updated
|
||||
StudyImportStatus.find(studyImportStatusId).observe({
|
||||
changed: function(studyImportStatus) {
|
||||
if (!studyImportStatus) {
|
||||
return;
|
||||
}
|
||||
|
||||
var numberOfStudiesProcessedToImport = studyImportStatus.numberOfStudiesImported + studyImportStatus.numberOfStudiesFailed;
|
||||
|
||||
// Show number of imported files
|
||||
var successMessage = 'Imported '+studyImportStatus.numberOfStudiesImported+' of '+numberOfStudiesToImport;
|
||||
progressDialog.setMessage({
|
||||
message: successMessage,
|
||||
messageType: 'success'
|
||||
});
|
||||
progressDialog.update(numberOfStudiesProcessedToImport);
|
||||
|
||||
// Show number of failed files if there is at least one failed file
|
||||
if (studyImportStatus.numberOfStudiesFailed > 0) {
|
||||
var successMessage = 'Failed '+studyImportStatus.numberOfStudiesFailed+' of '+numberOfStudiesToImport;
|
||||
progressDialog.setMessage({
|
||||
message: successMessage,
|
||||
messageType: 'warning'
|
||||
});
|
||||
}
|
||||
|
||||
if (numberOfStudiesProcessedToImport == numberOfStudiesToImport) {
|
||||
// The entire import operation is completed, so remove the study import status item
|
||||
Meteor.call("removeStudyImportStatus", studyImportStatus._id);
|
||||
|
||||
// Let the caller know that import operation is completed
|
||||
if (importCallback) {
|
||||
importCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Import studies with study import status id to get callbacks
|
||||
Meteor.call("importStudies", studiesToImport, studyImportStatusId);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Queries requested studies to get their metadata from PACS
|
||||
* @param studiesToQuery Studies to query
|
||||
* @param doneCallback Callback to call when the query is done
|
||||
*/
|
||||
queryStudies = function(studiesToQuery, doneCallback) {
|
||||
if (studiesToQuery.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var studiesQueried = [];
|
||||
var numberOfStudiesToQuery = studiesToQuery.length;
|
||||
|
||||
progressDialog.show("Querying Studies...", numberOfStudiesToQuery);
|
||||
|
||||
studiesToQuery.forEach(function(studyToQuery) {
|
||||
getStudyMetadata(studyToQuery.studyInstanceUid, function(study) {
|
||||
studiesQueried.push(study);
|
||||
|
||||
var numberOfStudiesQueried = studiesQueried.length;
|
||||
|
||||
progressDialog.update(numberOfStudiesQueried);
|
||||
|
||||
if (numberOfStudiesQueried === numberOfStudiesToQuery) {
|
||||
doneCallback(studiesQueried);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the total number of dicom files in a study
|
||||
* @param study Queried study (includes metadata)
|
||||
* @returns {number}
|
||||
*/
|
||||
getNumberOfFilesInStudy = function(study) {
|
||||
var numberOFFilesToExport = 0;
|
||||
|
||||
study.seriesList.forEach(function(series) {
|
||||
series.instances.forEach(function(instance) {
|
||||
if (instance.wadouri) {
|
||||
numberOFFilesToExport++;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return numberOFFilesToExport;
|
||||
};
|
||||
@@ -20,7 +20,6 @@ switchToTab = function(contentId) {
|
||||
} else {
|
||||
$('.tab-content .tab-pane#' + contentId).addClass('active');
|
||||
}
|
||||
|
||||
|
||||
// Remove any previous Viewers from the DOM
|
||||
$('.viewerContainer').remove();
|
||||
|
||||
@@ -73,11 +73,15 @@ Package.onUse(function (api) {
|
||||
api.addFiles('client/lib/switchToTab.js', 'client');
|
||||
api.addFiles('client/lib/exportStudies.js', 'client');
|
||||
api.addFiles('client/lib/worklist.js', 'client');
|
||||
api.addFiles('client/lib/queryStudies.js', 'client');
|
||||
api.addFiles('client/lib/importStudies.js', 'client');
|
||||
|
||||
// Server-side functions
|
||||
api.addFiles('server/collections.js', 'server');
|
||||
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/importStudies.js', 'server');
|
||||
api.addFiles('server/methods/worklistSearch.js', 'server');
|
||||
|
||||
// DICOMWeb instance, study, and metadata retrieval
|
||||
@@ -95,6 +99,8 @@ Package.onUse(function (api) {
|
||||
api.addFiles('server/services/remote/studies.js', 'server');
|
||||
api.addFiles('server/services/remote/retrieveMetadata.js', 'server');
|
||||
|
||||
api.addFiles('both/collections.js', [ 'client', 'server' ]);
|
||||
|
||||
api.export('Services', 'server');
|
||||
|
||||
// Export Worklist helper functions for usage in Routes
|
||||
@@ -113,5 +119,5 @@ Package.onUse(function (api) {
|
||||
// Export the Collections
|
||||
api.export('WorklistTabs', 'client');
|
||||
api.export('WorklistStudies', 'client');
|
||||
api.export('WorklistSelectedStudies', 'client');
|
||||
api.export('WorklistSelectedStudies', 'client')
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
Meteor.publish('studyImportStatus', function() {
|
||||
return StudyImportStatus.find();
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
var fs = Npm.require('fs');
|
||||
var fiber = Npm.require('fibers');
|
||||
|
||||
WebApp.connectHandlers.use('/uploadFilesToImport', function(req, res) {
|
||||
if (!req.headers.filename) {
|
||||
// Response: BAD REQUEST (400)
|
||||
res.statusCode = 400;
|
||||
res.end();
|
||||
}
|
||||
|
||||
// Store files in temp location (they will be deleted when their import operations are completed)
|
||||
var dicomDir = '/tmp/dicomDir';
|
||||
createFolderIfNotExist(dicomDir);
|
||||
|
||||
var fullFileName = dicomDir + '/' + req.headers.filename;
|
||||
var file = fs.createWriteStream(fullFileName);
|
||||
|
||||
file.on('error',function(error){
|
||||
console.log(error);
|
||||
// Response: INTERNAL SERVER ERROR (500)
|
||||
res.statusCode = 400;
|
||||
res.end();
|
||||
});
|
||||
file.on('finish',function(){
|
||||
// Response: SUCCESS (200)
|
||||
res.writeHead(200, {'Content-Type': 'text/plain'});
|
||||
res.end(fullFileName);
|
||||
});
|
||||
|
||||
// Pipe the request to the file
|
||||
req.pipe(file);
|
||||
});
|
||||
|
||||
Meteor.methods({
|
||||
/**
|
||||
* Returns true if import is supported for default service type
|
||||
* @returns {boolean}
|
||||
*/
|
||||
importSupported: function() {
|
||||
if (Meteor.settings.servers.dimse && Meteor.settings.defaultServiceType === 'dimse') {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Imports studies from local into worklist
|
||||
* @param studiesToImport Studies to import
|
||||
* @param studyImportStatusId Study import status collection id to track import status
|
||||
*/
|
||||
importStudies: function(studiesToImport, studyImportStatusId) {
|
||||
if (!studiesToImport || !studyImportStatusId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Meteor.settings.servers.dicomWeb && Meteor.settings.defaultServiceType === 'dicomWeb') {
|
||||
//TODO: Support importing studies into dicomWeb
|
||||
console.log('Importing studies into dicomWeb is currently not supported.');
|
||||
} else if (Meteor.settings.servers.dimse && Meteor.settings.defaultServiceType === 'dimse') {
|
||||
importStudiesDIMSE(studiesToImport, studyImportStatusId);
|
||||
} else {
|
||||
throw 'No properly configured server was available over DICOMWeb or DIMSE.';
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Create a new study import status item and insert it into the collection to track import status
|
||||
* @returns {studyImportStatusId: string}
|
||||
*/
|
||||
createStudyImportStatus: function() {
|
||||
var studyImportStatus = { numberOfStudiesImported: 0, numberOfStudiesFailed: 0 };
|
||||
return StudyImportStatus.insert(studyImportStatus);
|
||||
},
|
||||
/**
|
||||
* Remove the study import status item from the collection
|
||||
* @param id Collection id of the study import status in the collection
|
||||
*/
|
||||
removeStudyImportStatus: function(id) {
|
||||
StudyImportStatus.remove(id);
|
||||
}
|
||||
});
|
||||
|
||||
function importStudiesDIMSE(studiesToImport, studyImportStatusId) {
|
||||
if (!studiesToImport || !studyImportStatusId) {
|
||||
return;
|
||||
}
|
||||
// Perform C-Store to import studies and handle the callbacks to update import status
|
||||
DIMSE.storeInstances(studiesToImport, function(err, file) {
|
||||
try {
|
||||
// Use fiber to be able to modify meteor collection in callback
|
||||
fiber(function() {
|
||||
try {
|
||||
// Update the import status
|
||||
if (err) {
|
||||
StudyImportStatus.update({_id: studyImportStatusId}, {$inc: {'numberOfStudiesFailed': 1}});
|
||||
console.log("Failed to import study via DIMSE: ", file, err);
|
||||
} else {
|
||||
StudyImportStatus.update({_id: studyImportStatusId}, {$inc: {'numberOfStudiesImported': 1}});
|
||||
console.log("Study successfully imported via DIMSE: ", file);
|
||||
}
|
||||
|
||||
} catch(error) {
|
||||
|
||||
StudyImportStatus.update({_id: studyImportStatusId}, {$inc: {'numberOfStudiesFailed': 1}});
|
||||
console.log("Failed to import study via DIMSE: ", file, error);
|
||||
} finally {
|
||||
// The import operation of this file is completed, so delete it if still exists
|
||||
if (fileExists(file)) {
|
||||
fs.unlink(file);
|
||||
}
|
||||
}
|
||||
|
||||
}).run();
|
||||
} catch(error) {
|
||||
StudyImportStatus.update({_id: studyImportStatusId}, {$inc: {'numberOfStudiesFailed': 1}});
|
||||
console.log("Failed to import study via DIMSE: ", file, error);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function createFolderIfNotExist(folder) {
|
||||
var folderParts = folder.split('/');
|
||||
var folderPart = folderParts[0];
|
||||
for (var i = 1; i < folderParts.length; i++) {
|
||||
folderPart += '/' + folderParts[i];
|
||||
if (!folderExists(folderPart)) {
|
||||
fs.mkdirSync(folderPart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fileExists(folder) {
|
||||
try {
|
||||
return fs.statSync(folder).isFile();
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function folderExists(folder) {
|
||||
try {
|
||||
return fs.statSync(folder).isDirectory();
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ function resultDataToStudyMetadata(studyInstanceUid, resultData) {
|
||||
var instanceSummary = {
|
||||
imageType: instance[0x00080008],
|
||||
sopClassUid: instance[0x00080016],
|
||||
modality: instance[0x00080060],
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
instanceNumber: parseFloat(instance[0x00200013]),
|
||||
imagePositionPatient: instance[0x00200032],
|
||||
|
||||
@@ -82,6 +82,7 @@ function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
||||
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']),
|
||||
|
||||
Reference in new issue
Block a user