LT-366: StudyList :: Export workflow issues
This commit is contained in:
parent
87d61298b4
commit
79b2907085
@ -4,7 +4,11 @@
|
|||||||
title=(choose this.title 'Processing...')
|
title=(choose this.title 'Processing...')
|
||||||
)}}
|
)}}
|
||||||
<div class="status">
|
<div class="status">
|
||||||
<div class="progress-bar progress-bar-striped active" role="progressbar" style="width: {{progress}}%">{{formatNumberPrecision progress 0}}%</div>
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar progress-bar-striped active" role="progressbar" style="width: {{progress}}%">
|
||||||
|
<div class="percentage">{{formatNumberPrecision progress 0}}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="message">
|
<div class="message">
|
||||||
{{{message}}}
|
{{{message}}}
|
||||||
|
|||||||
@ -3,20 +3,15 @@ import { Session } from 'meteor/session';
|
|||||||
import { _ } from 'meteor/underscore';
|
import { _ } from 'meteor/underscore';
|
||||||
|
|
||||||
Template.dialogProgress.onCreated(() => {
|
Template.dialogProgress.onCreated(() => {
|
||||||
console.log('dialogProgress :: onCreated');
|
const instance = Template.instance();
|
||||||
});
|
|
||||||
|
|
||||||
Template.dialogProgress.onCreated(() => {
|
|
||||||
Session.set('progressDialogState', {
|
Session.set('progressDialogState', {
|
||||||
progress: 0,
|
processed: 0,
|
||||||
message: 'Test Message'
|
total: instance.data.total,
|
||||||
|
message: instance.data.message
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
Template.dialogProgress.onDestroyed(() => {
|
|
||||||
Session.set('progressDialogState', undefined);
|
|
||||||
})
|
|
||||||
|
|
||||||
Template.dialogProgress.onRendered(() => {
|
Template.dialogProgress.onRendered(() => {
|
||||||
const instance = Template.instance();
|
const instance = Template.instance();
|
||||||
const task = instance.data.task;
|
const task = instance.data.task;
|
||||||
@ -25,10 +20,10 @@ Template.dialogProgress.onRendered(() => {
|
|||||||
const progressDialog = {
|
const progressDialog = {
|
||||||
promise: instance.data.promise,
|
promise: instance.data.promise,
|
||||||
|
|
||||||
done: () => {
|
done: value => {
|
||||||
// Hide the modal, removing the backdrop
|
// Hide the modal, removing the backdrop
|
||||||
instance.$('.modal').on('hidden.bs.modal', event => {
|
instance.$('.modal').on('hidden.bs.modal', event => {
|
||||||
instance.data.promiseResolve();
|
instance.data.promiseResolve(value);
|
||||||
}).modal('hide');
|
}).modal('hide');
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -39,9 +34,16 @@ Template.dialogProgress.onRendered(() => {
|
|||||||
}).modal('hide');
|
}).modal('hide');
|
||||||
},
|
},
|
||||||
|
|
||||||
setProgress: _.throttle(progress => {
|
update: _.throttle(processed => {
|
||||||
const state = Session.get('progressDialogState');
|
const state = Session.get('progressDialogState');
|
||||||
state.progress = Math.min(1, progress);
|
state.processed = Math.max(0, processed);
|
||||||
|
|
||||||
|
Session.set('progressDialogState', state);
|
||||||
|
}, 100),
|
||||||
|
|
||||||
|
setTotal: _.throttle(total => {
|
||||||
|
const state = Session.get('progressDialogState');
|
||||||
|
state.total = total;
|
||||||
|
|
||||||
Session.set('progressDialogState', state);
|
Session.set('progressDialogState', state);
|
||||||
}, 100),
|
}, 100),
|
||||||
@ -54,21 +56,23 @@ Template.dialogProgress.onRendered(() => {
|
|||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
task.start(progressDialog);
|
task.run(progressDialog);
|
||||||
});
|
});
|
||||||
|
|
||||||
Template.dialogProgress.helpers({
|
Template.dialogProgress.helpers({
|
||||||
progress() {
|
progress() {
|
||||||
const state = Session.get('progressDialogState');
|
const state = Session.get('progressDialogState');
|
||||||
if (!state) {
|
|
||||||
return;
|
if (!state || !state.total) {
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return state.progress * 100;
|
return Math.min(1, state.processed / state.total) * 100;
|
||||||
},
|
},
|
||||||
|
|
||||||
message() {
|
message() {
|
||||||
const state = Session.get('progressDialogState');
|
const state = Session.get('progressDialogState');
|
||||||
|
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,14 @@
|
|||||||
.modal-progress
|
.modal-progress
|
||||||
.status
|
.status
|
||||||
.progress-bar
|
.progress-bar-container
|
||||||
float: none;
|
border: solid 1px #28405E
|
||||||
|
overflow: auto;
|
||||||
|
margin: 0 0 10px
|
||||||
|
border-radius: 3px
|
||||||
|
|
||||||
|
.percentage
|
||||||
|
margin: 0 5px
|
||||||
|
|
||||||
.btn-confirm
|
.btn-confirm
|
||||||
display: none
|
display: none
|
||||||
|
|
||||||
@ -1,7 +1,28 @@
|
|||||||
import { JSZip } from 'meteor/silentcicero:jszip';
|
import { JSZip } from 'meteor/silentcicero:jszip';
|
||||||
import { OHIF } from 'meteor/ohif:core';
|
import { OHIF } from 'meteor/ohif:core';
|
||||||
|
|
||||||
let exportFailed;
|
const getNumberOfFilesToExport = function(studiesToExport) {
|
||||||
|
let numberOfFilesToExport = 0;
|
||||||
|
|
||||||
|
studiesToExport.forEach(study => {
|
||||||
|
numberOfFilesToExport += getNumberOfFilesInStudy(study);
|
||||||
|
});
|
||||||
|
|
||||||
|
return numberOfFilesToExport;
|
||||||
|
}
|
||||||
|
|
||||||
|
const convertSizeToString = size => {
|
||||||
|
const measuments = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
let totalBytes = size,
|
||||||
|
measumentIndex = 0;
|
||||||
|
|
||||||
|
while(totalBytes > 1024) {
|
||||||
|
totalBytes /= 1024;
|
||||||
|
measumentIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${totalBytes.toFixed(2)} ${measuments[measumentIndex]}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exports requested studies
|
* Exports requested studies
|
||||||
@ -12,105 +33,170 @@ OHIF.studylist.exportStudies = studiesToExport => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
queryStudies(studiesToExport, exportQueriedStudies);
|
queryStudiesWithProgress(studiesToExport)
|
||||||
|
.then(exportQueriedStudiesWithProgress);
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportQueriedStudies = studiesToExport => {
|
const exportQueriedStudiesWithProgress = studiesToExport => {
|
||||||
let numberOfFilesToExport = 0;
|
const exportFilesCount = getNumberOfFilesToExport(studiesToExport);
|
||||||
studiesToExport.forEach(study => {
|
let exportHandler;
|
||||||
numberOfFilesToExport += getNumberOfFilesInStudy(study);
|
|
||||||
|
return OHIF.ui.showFormDialog('dialogProgress', {
|
||||||
|
title: 'Exporting Studies...',
|
||||||
|
message: `Exported files: 0 / exportFilesCount`,
|
||||||
|
total: getNumberOfFilesToExport(studiesToExport),
|
||||||
|
task: {
|
||||||
|
run: (dialog) => {
|
||||||
|
exportHandler = exportQueriedStudies(studiesToExport, {
|
||||||
|
notify: stats => {
|
||||||
|
const fileSize = convertSizeToString(stats.totalBytes);
|
||||||
|
|
||||||
|
dialog.update(stats.processed);
|
||||||
|
dialog.setMessage(`Exported files: ${stats.processed} / ${stats.total} (${fileSize})`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
OHIF.studylist.progressDialog.show('Exporting Studies...', numberOfFilesToExport);
|
exportHandler.promise.then(() => {
|
||||||
|
dialog.done();
|
||||||
try {
|
}, () => {
|
||||||
exportQueriedStudiesInternal(studiesToExport, numberOfFilesToExport);
|
dialog.setMessage('Failed to export studies');
|
||||||
} catch (err) {
|
});
|
||||||
OHIF.studylist.progressDialog.close();
|
|
||||||
OHIF.log.error(`Failed to export studies: ${err.message}`);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}).then(null, err => {
|
||||||
|
exportHandler.cancel();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportQueriedStudiesInternal = (studiesToExport, numberOfFilesToExport) => {
|
const exportQueriedStudies = (studiesToExport, options) => {
|
||||||
const zip = new JSZip();
|
const zip = new JSZip(),
|
||||||
|
promises = [],
|
||||||
|
pendingDownloads = [],
|
||||||
|
exportFilesCount = getNumberOfFilesToExport(studiesToExport),
|
||||||
|
notify = (options || {}).notify || function() { /* noop */ };
|
||||||
|
|
||||||
exportFailed = false;
|
const cancelDownloads = () => {
|
||||||
let numberOfFilesExported = 0;
|
while(pendingDownloads.length) {
|
||||||
|
const download = pendingDownloads.pop();
|
||||||
const onExportFailed = err => {
|
download.cancel();
|
||||||
exportFailed = true;
|
}
|
||||||
OHIF.studylist.progressDialog.close();
|
|
||||||
|
|
||||||
//TODO: Export failed and dialog closed, so let user know
|
|
||||||
OHIF.log.error('Failed to export studies!', err);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let totalBytes = 0;
|
||||||
|
|
||||||
studiesToExport.forEach(study => {
|
studiesToExport.forEach(study => {
|
||||||
sortStudy(study);
|
|
||||||
|
|
||||||
const studyFolder = zip.folder(study.studyInstanceUid);
|
|
||||||
|
|
||||||
study.seriesList.forEach(series => {
|
study.seriesList.forEach(series => {
|
||||||
const seriesFolder = studyFolder.folder(series.seriesInstanceUid);
|
|
||||||
|
|
||||||
series.instances.forEach(instance => {
|
series.instances.forEach(instance => {
|
||||||
if (!instance.wadouri) {
|
if (!instance.wadouri) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If failed to download a dicom file, skip others
|
const download = downloadDicomFile(instance);
|
||||||
if (exportFailed) {
|
pendingDownloads.push(download);
|
||||||
return;
|
|
||||||
|
const promise = download.promise
|
||||||
|
.then(data => {
|
||||||
|
const downloadIndex = pendingDownloads.indexOf(download);
|
||||||
|
|
||||||
|
totalBytes += data && data.size ? data.size : 0;
|
||||||
|
|
||||||
|
if(downloadIndex > -1) {
|
||||||
|
pendingDownloads.splice(downloadIndex, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download and Zip the dicom file
|
notify({
|
||||||
const xhr = new XMLHttpRequest();
|
total: exportFilesCount,
|
||||||
|
processed: exportFilesCount - pendingDownloads.length,
|
||||||
|
totalBytes: totalBytes
|
||||||
|
})
|
||||||
|
|
||||||
|
return zipInstance(study, series, instance, zip, data)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if(!(err instanceof ExportStudyDownloadCanceledError)) {
|
||||||
|
OHIF.log.error('Failed to export studies', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelDownloads();
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
promises.push(promise);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
cancel: cancelDownloads,
|
||||||
|
promise: Promise.all(promises).then(() => {
|
||||||
|
const zipContent = zip.generate({ type: 'blob' });
|
||||||
|
saveAs(zipContent, 'studies.zip');
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadDicomFile = instance => {
|
||||||
|
let xhr,
|
||||||
|
promiseReject;
|
||||||
|
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
promiseReject = reject;
|
||||||
|
|
||||||
|
xhr = new XMLHttpRequest();
|
||||||
xhr.open('GET', instance.wadouri, true);
|
xhr.open('GET', instance.wadouri, true);
|
||||||
xhr.responseType = 'blob';
|
xhr.responseType = 'blob';
|
||||||
|
|
||||||
// Downloaded the dicom file completely
|
|
||||||
xhr.onload = () => {
|
xhr.onload = () => {
|
||||||
// If failed to download a dicom file, skip others
|
|
||||||
if (exportFailed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Failed to export a file
|
|
||||||
if (xhr.readyState === 4 && xhr.status !== 200) {
|
if (xhr.readyState === 4 && xhr.status !== 200) {
|
||||||
onExportFailed(`File not downloaded: ${instance.wadouri}`);
|
return reject(new Error(`File not downloaded: ${instance.wadouri}`));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const blobFile = new Blob([xhr.response], { type: 'application/dicom' });
|
resolve(xhr.response)
|
||||||
|
|
||||||
const fileReader = new FileReader();
|
|
||||||
|
|
||||||
fileReader.onload = () => {
|
|
||||||
try {
|
|
||||||
seriesFolder.file(`${instance.sopInstanceUid}.dcm`, fileReader.result, { binary: true });
|
|
||||||
} catch(err) {
|
|
||||||
onExportFailed(err.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
numberOfFilesExported++;
|
|
||||||
|
|
||||||
if (numberOfFilesExported === numberOfFilesToExport) {
|
|
||||||
const zipContent = zip.generate({ type: 'blob' });
|
|
||||||
saveAs(zipContent, 'studies.zip');
|
|
||||||
}
|
|
||||||
|
|
||||||
OHIF.studylist.progressDialog.update(numberOfFilesExported);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fileReader.readAsArrayBuffer(blobFile);
|
xhr.onerror = () => {
|
||||||
|
reject(new Error(`File not downloaded: ${instance.wadouri}`));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Failed to download the dicom file
|
|
||||||
xhr.onerror = () => onExportFailed(`File not downloaded: ${instance.wadouri}`);
|
|
||||||
|
|
||||||
xhr.send();
|
xhr.send();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
return {
|
||||||
|
promise: promise,
|
||||||
|
cancel: () => {
|
||||||
|
xhr.abort();
|
||||||
|
promiseReject(new ExportStudyDownloadCanceledError('Download canceled'));
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const zipInstance = (study, series, instance, zip, data) => {
|
||||||
|
const fileReader = new FileReader(),
|
||||||
|
blobFile = new Blob([data], { type: 'application/dicom' }),
|
||||||
|
zipFolder = zip.folder(study.studyInstanceUid).folder(series.seriesInstanceUid);
|
||||||
|
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
fileReader.onload = () => {
|
||||||
|
try {
|
||||||
|
zipFolder.file(`${instance.sopInstanceUid}.dcm`, fileReader.result, { binary: true });
|
||||||
|
resolve()
|
||||||
|
} catch(err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
fileReader.readAsArrayBuffer(blobFile);
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const ExportStudyDownloadCanceledError = (message) => {
|
||||||
|
this.name = 'ExportStudyDownloadCanceledError';
|
||||||
|
this.message = message || 'Download canceled';
|
||||||
|
this.stack = (new Error()).stack;
|
||||||
|
}
|
||||||
|
ExportStudyDownloadCanceledError.prototype = Object.create(Error.prototype);
|
||||||
|
ExportStudyDownloadCanceledError.prototype.constructor = ExportStudyDownloadCanceledError;
|
||||||
|
|||||||
@ -3,31 +3,55 @@ import { OHIF } from 'meteor/ohif:core';
|
|||||||
/**
|
/**
|
||||||
* Queries requested studies to get their metadata from PACS
|
* Queries requested studies to get their metadata from PACS
|
||||||
* @param studiesToQuery Studies to query
|
* @param studiesToQuery Studies to query
|
||||||
* @param doneCallback Callback to call when the query is done
|
|
||||||
*/
|
*/
|
||||||
queryStudies = function(studiesToQuery, doneCallback) {
|
queryStudies = function(studiesToQuery, options) {
|
||||||
|
const studiesQueried = [],
|
||||||
|
numberOfStudiesToQuery = studiesToQuery.length,
|
||||||
|
notify = (options || {}).notify || function() { /* noop */ }
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
if (studiesToQuery.length < 1) {
|
if (studiesToQuery.length < 1) {
|
||||||
return;
|
return reject();
|
||||||
}
|
}
|
||||||
|
|
||||||
var studiesQueried = [];
|
|
||||||
var numberOfStudiesToQuery = studiesToQuery.length;
|
|
||||||
|
|
||||||
OHIF.studylist.progressDialog.show("Querying Studies...", numberOfStudiesToQuery);
|
|
||||||
|
|
||||||
studiesToQuery.forEach(function(studyToQuery) {
|
studiesToQuery.forEach(function(studyToQuery) {
|
||||||
getStudyMetadata(studyToQuery.studyInstanceUid, function(study) {
|
getStudyMetadata(studyToQuery.studyInstanceUid, function(study) {
|
||||||
studiesQueried.push(study);
|
studiesQueried.push(study);
|
||||||
|
|
||||||
var numberOfStudiesQueried = studiesQueried.length;
|
notify({
|
||||||
|
total: numberOfStudiesToQuery,
|
||||||
|
processed: studiesQueried.length
|
||||||
|
});
|
||||||
|
|
||||||
OHIF.studylist.progressDialog.update(numberOfStudiesQueried);
|
if (studiesQueried.length === numberOfStudiesToQuery) {
|
||||||
|
resolve(studiesQueried);
|
||||||
if (numberOfStudiesQueried === numberOfStudiesToQuery) {
|
|
||||||
doneCallback(studiesQueried);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
queryStudiesWithProgress = function(studiesToQuery) {
|
||||||
|
return OHIF.ui.showFormDialog('dialogProgress', {
|
||||||
|
title: 'Querying Studies...',
|
||||||
|
message: `Queried: 0 / ${studiesToQuery.length}`,
|
||||||
|
total: studiesToQuery.length,
|
||||||
|
task: {
|
||||||
|
run: (dialog) => {
|
||||||
|
queryStudies(studiesToQuery, {
|
||||||
|
notify: stats => {
|
||||||
|
dialog.update(stats.processed);
|
||||||
|
dialog.setMessage(`Queried: ${stats.processed} / ${stats.total}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(studiesQueried => {
|
||||||
|
dialog.done(studiesQueried);
|
||||||
|
}, () => {
|
||||||
|
dialog.cancel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
* A global Blaze UI helper to format a float value to a specified precision
|
* A global Blaze UI helper to format a float value to a specified precision
|
||||||
*/
|
*/
|
||||||
UI.registerHelper('formatNumberPrecision', function(context, precision) {
|
UI.registerHelper('formatNumberPrecision', function(context, precision) {
|
||||||
if (context) {
|
if (context != null) {
|
||||||
return parseFloat(context).toFixed(precision);
|
return parseFloat(context).toFixed(precision);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Loading…
Reference in New Issue
Block a user