LT-66 Import DICOM Studies

- Add a toolbar in the top of worklist
- Add Import button in the worklist toolbar
- Upload all files in the selected folder to import from client to server
- Import uploaded files via DIMSE
- Delete uploaded files from server when import is completed
- Handle import status in client
- Show/Update progress dialog in client while uploading and importing files
- Show Import button only if import is supported for the default service type (DICOMWeb is currently not supported)
This commit is contained in:
Evren Ozkan committed 2016-03-30 18:38:29 -04:00
1 parent 064743a8f7
commit a2124c03e9
13 files changed
+324 -3

No files matched your search

+1
View File
@@ -0,0 +1 @@
StudyImportStatus = new Meteor.Collection('studyImportStatus');
@@ -2,3 +2,5 @@ ViewerStudies = new Meteor.Collection(null);
ViewerStudies._debugName = 'ViewerStudies';
ClientId = Random.id();
Meteor.subscribe('studyImportStatus');
+107
View File
@@ -0,0 +1,107 @@
/**
* 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("Uploading Files...", 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
console.log("Failed to upload " + studyImportStatus.numberOfStudiesFailed + " of " + numberOfStudiesToImport + " 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) {
var numberOfStudiesToImport = studiesToImport.length;
progressDialog.show("Importing Studies...", numberOfStudiesToImport);
// Create/Insert a new study import status item
Meteor.call("createStudyImportStatus", function(err, studyImportStatusId) {
if (err) {
console.log(err);
return;
}
// Handle when it is updated
StudyImportStatus.find(studyImportStatusId).observe({
changed: function(studyImportStatus) {
if (!studyImportStatus) {
return;
}
var numberOfStudiesProcessedToImport = studyImportStatus.numberOfStudiesImported + studyImportStatus.numberOfStudiesFailed;
progressDialog.update(numberOfStudiesProcessedToImport);
if (numberOfStudiesProcessedToImport == numberOfStudiesToImport) {
// The entire import operation is completed, so remove the study import status item
Meteor.call("removeStudyImportStatus", studyImportStatus._id);
if (studyImportStatus.numberOfStudiesFailed > 0) {
//TODO: Some files failed to import, so let user know
console.log("Failed to import " + studyImportStatus.numberOfStudiesFailed + " of " + numberOfStudiesToImport + " files");
}
// 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);
});
}
+10
View File
@@ -147,6 +147,7 @@ Package.onUse(function(api) {
api.addFiles('lib/setFocusToActiveViewport.js', 'client');
api.addFiles('lib/updateAllViewports.js', 'client');
api.addFiles('lib/exportStudies.js', 'client');
api.addFiles('lib/importStudies.js', 'client');
api.addFiles('lib/encodeQueryData.js', 'server');
//api.export('accountsConfig', 'client');
@@ -166,6 +167,7 @@ Package.onUse(function(api) {
api.export('setFocusToActiveViewport', 'client');
api.export('updateAllViewports', 'client');
api.export('exportStudies', 'client');
api.export('importStudies', 'client');
api.export('getActiveViewportElement', 'client');
api.export('encodeQueryData', 'server');
@@ -198,6 +200,7 @@ Package.onUse(function(api) {
api.addFiles('server/lib/namespace.js', 'server');
api.addFiles('server/methods/getStudyMetadata.js', 'server');
api.addFiles('server/methods/worklistSearch.js', 'server');
api.addFiles('server/methods/importStudies.js', 'server');
// DICOMWeb instance, study, and metadata retrieval
api.addFiles('server/services/qido/instances.js', 'server');
@@ -215,5 +218,12 @@ Package.onUse(function(api) {
api.addFiles('server/services/remote/retrieveMetadata.js', 'server');
api.export('Services', 'server');
api.export('importStudies', 'server');
api.export('importSupported', 'server');
// Collections
api.addFiles('both/collections.js', [ 'client', 'server' ]);
api.addFiles('server/collections.js', 'server');
api.export('StudyImportStatus', [ 'client', 'server' ]);
});
@@ -0,0 +1,3 @@
Meteor.publish('studyImportStatus', function() {
return StudyImportStatus.find();
});
@@ -0,0 +1,125 @@
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.dimse && Meteor.settings.defaultServiceType === 'dimse') {
return true;
}
//TODO: Support importing studies into dicomWeb
return false;
},
/**
* 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 (Meteor.settings.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.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) {
// Perform C-Store to import studies and handle the callbacks to update import status
DIMSE.storeInstances(studiesToImport, function(err, file) {
// Use fiber to be able to modify meteor collection in callback
fiber(function() {
// 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);
}
// The import operation of this file is completed, so delete it if still exists
if (fileExists(file)) {
fs.unlink(file);
}
}).run();
});
}
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;
}
}