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:
parent
064743a8f7
commit
a2124c03e9
@ -115,6 +115,7 @@
|
|||||||
ViewerStudies: true,
|
ViewerStudies: true,
|
||||||
LesionManager: true,
|
LesionManager: true,
|
||||||
Timepoints: true,
|
Timepoints: true,
|
||||||
Measurements: true
|
Measurements: true,
|
||||||
|
StudyImportStatus: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -201,10 +201,10 @@ DIMSE.retrieveInstances = function(studyInstanceUID, seriesInstanceUID, params,
|
|||||||
return future.wait();
|
return future.wait();
|
||||||
};
|
};
|
||||||
|
|
||||||
DIMSE.storeInstances = function(fileList) {
|
DIMSE.storeInstances = function(fileList, callback) {
|
||||||
var handle = conn.storeInstances(fileList);
|
var handle = conn.storeInstances(fileList);
|
||||||
handle.on('file', function(err, file) {
|
handle.on('file', function(err, file) {
|
||||||
console.log(err, file);
|
callback(err, file);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
1
Packages/viewerbase/both/collections.js
Normal file
1
Packages/viewerbase/both/collections.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
StudyImportStatus = new Meteor.Collection('studyImportStatus');
|
||||||
@ -2,3 +2,5 @@ ViewerStudies = new Meteor.Collection(null);
|
|||||||
ViewerStudies._debugName = 'ViewerStudies';
|
ViewerStudies._debugName = 'ViewerStudies';
|
||||||
|
|
||||||
ClientId = Random.id();
|
ClientId = Random.id();
|
||||||
|
|
||||||
|
Meteor.subscribe('studyImportStatus');
|
||||||
107
Packages/viewerbase/lib/importStudies.js
Normal file
107
Packages/viewerbase/lib/importStudies.js
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -147,6 +147,7 @@ Package.onUse(function(api) {
|
|||||||
api.addFiles('lib/setFocusToActiveViewport.js', 'client');
|
api.addFiles('lib/setFocusToActiveViewport.js', 'client');
|
||||||
api.addFiles('lib/updateAllViewports.js', 'client');
|
api.addFiles('lib/updateAllViewports.js', 'client');
|
||||||
api.addFiles('lib/exportStudies.js', 'client');
|
api.addFiles('lib/exportStudies.js', 'client');
|
||||||
|
api.addFiles('lib/importStudies.js', 'client');
|
||||||
api.addFiles('lib/encodeQueryData.js', 'server');
|
api.addFiles('lib/encodeQueryData.js', 'server');
|
||||||
|
|
||||||
//api.export('accountsConfig', 'client');
|
//api.export('accountsConfig', 'client');
|
||||||
@ -166,6 +167,7 @@ Package.onUse(function(api) {
|
|||||||
api.export('setFocusToActiveViewport', 'client');
|
api.export('setFocusToActiveViewport', 'client');
|
||||||
api.export('updateAllViewports', 'client');
|
api.export('updateAllViewports', 'client');
|
||||||
api.export('exportStudies', 'client');
|
api.export('exportStudies', 'client');
|
||||||
|
api.export('importStudies', 'client');
|
||||||
api.export('getActiveViewportElement', 'client');
|
api.export('getActiveViewportElement', 'client');
|
||||||
api.export('encodeQueryData', 'server');
|
api.export('encodeQueryData', 'server');
|
||||||
|
|
||||||
@ -198,6 +200,7 @@ Package.onUse(function(api) {
|
|||||||
api.addFiles('server/lib/namespace.js', 'server');
|
api.addFiles('server/lib/namespace.js', 'server');
|
||||||
api.addFiles('server/methods/getStudyMetadata.js', 'server');
|
api.addFiles('server/methods/getStudyMetadata.js', 'server');
|
||||||
api.addFiles('server/methods/worklistSearch.js', 'server');
|
api.addFiles('server/methods/worklistSearch.js', 'server');
|
||||||
|
api.addFiles('server/methods/importStudies.js', 'server');
|
||||||
|
|
||||||
// DICOMWeb instance, study, and metadata retrieval
|
// DICOMWeb instance, study, and metadata retrieval
|
||||||
api.addFiles('server/services/qido/instances.js', 'server');
|
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.addFiles('server/services/remote/retrieveMetadata.js', 'server');
|
||||||
|
|
||||||
api.export('Services', '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' ]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
3
Packages/viewerbase/server/collections.js
Normal file
3
Packages/viewerbase/server/collections.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Meteor.publish('studyImportStatus', function() {
|
||||||
|
return StudyImportStatus.find();
|
||||||
|
});
|
||||||
125
Packages/viewerbase/server/methods/importStudies.js
Normal file
125
Packages/viewerbase/server/methods/importStudies.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
<template name="worklistResult">
|
<template name="worklistResult">
|
||||||
|
{{>worklistToolbar}}
|
||||||
<table id="tblStudyList" class="worklistResult table table-striped noselect">
|
<table id="tblStudyList" class="worklistResult table table-striped noselect">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
<template name="worklistToolbar">
|
||||||
|
<div id='worklistToolbar'>
|
||||||
|
<div class="btn-group">
|
||||||
|
{{#if importSupported }}
|
||||||
|
<span class="btn btn-default btn-file">
|
||||||
|
<i class="fa fa-upload"></i>
|
||||||
|
<input id="btnImport" title="Import" type="file" webkitdirectory directory multiple>
|
||||||
|
</span>
|
||||||
|
{{/if }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
Template.worklistToolbar.events({
|
||||||
|
'change #btnImport': function(e) {
|
||||||
|
// Get selected files located in the client machine
|
||||||
|
var selectedFiles = $.map(e.currentTarget.files, function(value) {
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
|
||||||
|
importStudies(selectedFiles);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Template.worklistToolbar.helpers({
|
||||||
|
importSupported: function() {
|
||||||
|
var importSupported = Session.get('importSupported');
|
||||||
|
if (importSupported) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Meteor.call("importSupported", function(err, result) {
|
||||||
|
if (!err && result) {
|
||||||
|
Session.set('importSupported', true);
|
||||||
|
} else {
|
||||||
|
Session.set('importSupported', false);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
#worklistToolbar
|
||||||
|
background-color: black
|
||||||
|
text-align: right
|
||||||
|
width: 100%
|
||||||
|
height: auto
|
||||||
|
|
||||||
|
.btn-file
|
||||||
|
position: relative
|
||||||
|
overflow: hidden
|
||||||
|
width: 30px
|
||||||
|
height: 30px
|
||||||
|
padding: 5px
|
||||||
|
line-height: 20px
|
||||||
|
|
||||||
|
input[type=file]
|
||||||
|
position: absolute
|
||||||
|
top: 0
|
||||||
|
right: 0
|
||||||
|
min-width: 100%
|
||||||
|
min-height: 100%
|
||||||
|
font-size: 100 px
|
||||||
|
text-align: right
|
||||||
|
opacity: 0
|
||||||
|
outline: none
|
||||||
|
background: white
|
||||||
|
cursor: inherit
|
||||||
|
display: block
|
||||||
@ -47,6 +47,10 @@ Package.onUse(function (api) {
|
|||||||
api.addFiles('client/components/studyContextMenu/studyContextMenu.js', 'client');
|
api.addFiles('client/components/studyContextMenu/studyContextMenu.js', 'client');
|
||||||
api.addFiles('client/components/studyContextMenu/studyContextMenu.styl', 'client');
|
api.addFiles('client/components/studyContextMenu/studyContextMenu.styl', 'client');
|
||||||
|
|
||||||
|
api.addFiles('client/components/worklistToolbar/worklistToolbar.html', 'client');
|
||||||
|
api.addFiles('client/components/worklistToolbar/worklistToolbar.js', 'client');
|
||||||
|
api.addFiles('client/components/worklistToolbar/worklistToolbar.styl', 'client');
|
||||||
|
|
||||||
// Library functions
|
// Library functions
|
||||||
api.addFiles('lib/getStudyMetadata.js', 'client');
|
api.addFiles('lib/getStudyMetadata.js', 'client');
|
||||||
api.addFiles('lib/getStudiesMetadata.js', 'client');
|
api.addFiles('lib/getStudiesMetadata.js', 'client');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user