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:
Erik Ziegler committed 2016-08-08 18:37:32 +02:00
1 parent 9d8c039ffc
commit fe32c40832
32 files changed
+231 -793

No files matched your search

+79 -18
View File
@@ -11,32 +11,93 @@
* @returns {Array} An array of series to be placed in the Study Browser
*/
createStacks = function(study) {
// Define an empty array of stacks
var stacks = [];
// Define an empty array of display sets
var displaySets = [];
if (!study || !study.seriesList) {
return stacks;
return displaySets;
}
// TODO: Split by multi-frame, modality, image size, etc
study.seriesList.forEach(function(series) {
study.seriesList.forEach(series => {
// If the series has no instances, skip it
if (!series.instances) {
return;
}
// Don't display thumbnails for non-image modalities
// All imaging modalities must have a valid value for rows (or columns)
var anInstance = series.instances[0];
if (!anInstance || !anInstance.rows) {
return;
// Search through the instances of this series
// Split Multi-frame instances and Single-image modalities
// into their own specific display sets. Place the rest of each
// series into another display set.
let stackableInstances = [];
series.instances.forEach(instance => {
// All imaging modalities must have a valid value for rows (or columns)
if (!instance.rows) {
return;
}
let displaySet;
if (isMultiFrame(instance)) {
displaySet = makeDisplaySet(series, [ instance ]);
displaySet.isClip = true;
// Include the study instance Uid for drag/drop purposes
displaySet.studyInstanceUid = study.studyInstanceUid;
// Override the default value of instances.length
displaySet.numImageFrames = instance.numFrames;
displaySets.push(displaySet);
} else if (isSingleImageModality(instance.modality)) {
displaySet = makeDisplaySet(series, [ instance ]);
displaySet.studyInstanceUid = study.studyInstanceUid;
displaySets.push(displaySet);
} else {
stackableInstances.push(instance);
}
});
if (stackableInstances.length) {
let displaySet = makeDisplaySet(series, stackableInstances);
displaySet.studyInstanceUid = study.studyInstanceUid;
displaySets.push(displaySet);
}
// Include the study instance Uid for drag/drop purposes
series.studyInstanceUid = study.studyInstanceUid;
// Add this series to the list of stacks
stacks.push(series);
});
return stacks;
};
return displaySets;
};
function makeDisplaySet(series, instances) {
const instance = instances[0];
let displaySet = {
seriesInstanceUid: series.seriesInstanceUid,
seriesNumber: series.seriesNumber,
seriesDescription: series.seriesDescription,
numImageFrames: instances.length,
frameRate: instance.frameTime,
images: instances
};
// Sort the images in this series
displaySet.images.sort(function(a, b) {
if (a.instanceNumber && b.instanceNumber &&
a.instanceNumber !== b.instanceNumber) {
return a.instanceNumber - b.instanceNumber;
}
});
// Create a unique ID for this stack so we can reference it
displaySet.displaySetInstanceUid = Random.id();
return displaySet;
}
function isSingleImageModality(modality) {
return (modality === 'CR' ||
modality === 'MG' ||
modality === 'DX');
}
function isMultiFrame(instance) {
return instance.numFrames > 1;
}
@@ -1,7 +1,7 @@
/**
* Boolean helper to identify if a series instance is active in some viewport
*/
Template.registerHelper('isSeriesActive', (seriesInstanceUid, viewportIndex) => {
Template.registerHelper('isDisplaySetActive', (displaySetInstanceUid, viewportIndex) => {
// Run this computation every time the viewports are updated
Session.get('LayoutManagerUpdated');
@@ -19,7 +19,7 @@ Template.registerHelper('isSeriesActive', (seriesInstanceUid, viewportIndex) =>
let result = false;
_.each(viewportData, data => {
if (data && data.seriesInstanceUid === seriesInstanceUid) {
if (data && data.displaySetInstanceUid === displaySetInstanceUid) {
result = true;
}
});
@@ -2,8 +2,8 @@
* A global Blaze UI helper to get the thumbnails for the given study
*/
Template.registerHelper('studyThumbnails', study => {
// Creates the series stacks
const stacks = createStacks(study);
// Find the study's stacks
const stacks = study.displaySets;
// Defines the resulting thumbnails list
const thumbnails = [];
-131
View File
@@ -1,131 +0,0 @@
/**
* 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);
});
}
+11 -9
View File
@@ -27,8 +27,8 @@ LayoutManager = class LayoutManager {
var viewportIndex = 0;
var self = this;
this.studies.forEach(function(study) {
study.seriesList.forEach(function(series) {
if (!series.instances.length) {
study.displaySets.forEach(function(displaySet) {
if (!displaySet.images.length) {
return;
}
@@ -39,6 +39,7 @@ LayoutManager = class LayoutManager {
viewportIndex: existingViewportData.viewportIndex,
studyInstanceUid: existingViewportData.studyInstanceUid,
seriesInstanceUid: existingViewportData.seriesInstanceUid,
displaySetInstanceUid: existingViewportData.displaySetInstanceUid,
sopInstanceUid: existingViewportData.sopInstanceUid,
viewport: existingViewportData.viewport,
imageId: existingViewportData.imageId,
@@ -50,7 +51,7 @@ LayoutManager = class LayoutManager {
// This tests to make sure there is actually image data in this instance
// TODO: Change this when we add PDF and MPEG support
// See https://ohiforg.atlassian.net/browse/LT-227
var firstInstance = series.instances[0];
var firstInstance = displaySet.images[0];
// All imaging modalities must have a valid value for sopClassUid or rows
if (!firstInstance || (!isImage(firstInstance.sopClassUid) && !firstInstance.rows)) {
@@ -59,7 +60,8 @@ LayoutManager = class LayoutManager {
currentViewportData = {
viewportIndex: viewportIndex,
studyInstanceUid: study.studyInstanceUid,
seriesInstanceUid: series.seriesInstanceUid,
seriesInstanceUid: displaySet.seriesInstanceUid,
displaySetInstanceUid: displaySet.displaySetInstanceUid,
sopInstanceUid: firstInstance.sopInstanceUid,
currentImageIdIndex: 0 // TODO Remove this once currentImageIdIndex is removed from imageViewerViewports
};
@@ -114,13 +116,12 @@ LayoutManager = class LayoutManager {
/**
* This function destroys and re-renders the imageViewerViewport template.
* It uses the data provided (object containing seriesIndex and studyIndex) to
* load a new series into the produced viewport.
* It uses the data provided to load a new display set into the produced viewport.
*
* @param viewportIndex
* @param data An object containing a seriesIndex and studyIndex for a study to load into this viewport
* @param data
*/
rerenderViewportWithNewSeries(viewportIndex, data) {
rerenderViewportWithNewDisplaySet(viewportIndex, data) {
// The parent container is identified because it is later removed from the DOM
var element = $('.imageViewerViewport').eq(viewportIndex);
var container = element.parents('.viewportContainer').get(0);
@@ -128,9 +129,10 @@ LayoutManager = class LayoutManager {
// Record the current viewportIndex so this can be passed into the re-rendering call
data.viewportIndex = viewportIndex;
// Update the dictionary of loaded series for the specified viewport
// Update the dictionary of loaded displaySet for the specified viewport
this.viewportData[viewportIndex] = {
viewportIndex: viewportIndex,
displaySetInstanceUid: data.displaySetInstanceUid,
seriesInstanceUid: data.seriesInstanceUid,
studyInstanceUid: data.studyInstanceUid,
renderedCallback: data.renderedCallback,
-48
View File
@@ -1,48 +0,0 @@
/**
* 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;
};