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
@@ -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;
|
||||
}
|
||||
+2
-2
@@ -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 = [];
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
Reference in new issue
Block a user