First pass at a Hanging Protocol implementation and editor (LT-131)

This commit is contained in:
Erik Ziegler committed 2016-05-07 12:40:02 +02:00
1 parent dfcb33153f
commit 2e52d9e3c1
157 files changed
+8869 -23585

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,16 @@
<template name="confirmDeleteDialog">
<div id="confirmDeleteDialog" tabindex="0">
{{ #if title }}
<h5>{{title}}</h5>
{{ else }}
<h5>Remove data from Timepoint?</h5>
{{/if}}
{{ #if text }}
<p>{{text}}</p>
{{ else }}
<p>Are you sure you would like to remove the measurement data at this timepoint?</p>
{{/if}}
<button id="cancel" class="btn btn-link" tabindex="1">Cancel</button>
<button id="confirm" class="btn btn-primary" tabindex="0">OK</button>
</div>
</template>
@@ -0,0 +1,85 @@
function closeHandler() {
// Hide the lesion dialog
$('#confirmDeleteDialog').css('display', 'none');
// Remove the backdrop
$('.removableBackdrop').remove();
// Remove the callback from the template data
delete Template.confirmDeleteDialog.doneCallback;
// Restore the focus to the active viewport
setFocusToActiveViewport();
}
/**
* Displays the confirmation dialog template and the removable backdrop element
*
* @param doneCallback A callback
* @param options
*/
showConfirmDialog = function(doneCallback, options) {
// Show the backdrop
options = options || {};
UI.renderWithData(Template.removableBackdrop, options, document.body);
var confirmDeleteDialog = $('#confirmDeleteDialog');
confirmDeleteDialog.remove();
var viewer = document.getElementById('viewer');
UI.renderWithData(Template.confirmDeleteDialog, options, viewer);
// Make sure the context menu is closed when the user clicks away
$('.removableBackdrop').one('mousedown touchstart', function() {
closeHandler();
});
confirmDeleteDialog = $('#confirmDeleteDialog');
confirmDeleteDialog.css('display', 'block');
confirmDeleteDialog.focus();
if (doneCallback && typeof doneCallback === 'function') {
Template.confirmDeleteDialog.doneCallback = doneCallback;
}
};
// Global object of key names (TODO: put this somewhere else)
keys = {
ESC: 27,
ENTER: 13
};
Template.confirmDeleteDialog.events({
'click #cancel, click #close': function() {
closeHandler();
},
'click #confirm': function() {
var doneCallback = Template.confirmDeleteDialog.doneCallback;
if (doneCallback && typeof doneCallback === 'function') {
doneCallback();
}
closeHandler();
},
'keydown #confirmDeleteDialog': function(e) {
if (e.which === keys.ESC) {
closeHandler();
return false;
}
if (this.keyPressAllowed === false) {
return;
}
var doneCallback = Template.confirmDeleteDialog.doneCallback;
// If Enter is pressed, close the dialog
if (e.which === keys.ENTER) {
if (doneCallback && typeof doneCallback === 'function') {
doneCallback();
}
closeHandler();
}
}
});
@@ -1,25 +1,26 @@
#progressDialog
#confirmDeleteDialog
display: none
position: absolute
top: 0
bottom: 0
left: 0
right: 0
margin: auto
width: 20%
height: 20%
z-index: 100
width: 300px
min-height: 145px
max-height: 300px
height: fit-content
margin: auto
border-radius: 5px
padding: 10px 20px 10px 20px
background-color: rgba(255,255,255,1)
outline: none
.dialogContent
margin-bottom: 20px
.btn
text-decoration: none
#message
width: 100%
#cancel
float: left
span
display: inline-block;
width: 100%
#confirm
float: right
@@ -1,20 +0,0 @@
<template name="hidingPanel">
<div class="hidingPanel {{#if isPinned}}pinned{{/if}}">
<div class="iconContainer">
{{#unless isPinned}}
<i class="arrowIcon fa fa-angle-double-right"></i>
{{/unless}}
<button class="btn btn-sm btn-primary btnPin {{#if isPinned}}active{{/if}}">
<i class="fa fa-thumb-tack"></i>
</button>
</div>
<div class="hidingPanelContent">
{{#if studyDateIsShown}}
{{> studyDateList}}
{{/if}}
<div class="studyBrowserContainer">
{{> studyBrowser}}
</div>
</div>
</div>
</template>
@@ -1,123 +0,0 @@
function pinPanel() {
var hidingPanel = $(".hidingPanel");
hidingPanel.css({
width: '122px'
});
hidingPanel.find('.hidingPanelContent').css({
transform: 'translate(130px)'
});
// Calculate newWidth of viewportAndLesionTable
var viewerWidth = $('#viewer').width();
var newWidth = 100 - 122 / viewerWidth * 100;
$("#viewportAndLesionTable").css('width', newWidth + '%');
}
function unpinPanel() {
// Calculate newWidth of viewportAndLesionTable
$("#viewportAndLesionTable").css('width', '99%');
}
Template.hidingPanel.events({
'mouseover div.hidingPanel': function(e, template) {
var isPinned = template.isPinned.get();
if (isPinned) {
return;
}
var hidingPanel = $(e.currentTarget);
hidingPanel.css({
width: '122px'
});
// Set panel as open
template.panelOpen.set(true);
// Rotate Arrow Icon
$('.arrowIcon').addClass('fa-flip-horizontal');
// Set panel content opacity
//hidingPanel.find('.hidingPanelContent').css('opacity', '1');
hidingPanel.find('.hidingPanelContent').css({
transform: 'translate(130px)'
});
},
'mouseout div.hidingPanel': function(e, template) {
var isPinned = template.isPinned.get();
if (isPinned) {
return;
}
var hidingPanel = $(e.currentTarget);
hidingPanel.css({
width: '1%'
});
// Set panel as closed
template.panelOpen.set(false);
// Rotate Arrow Icon
$('.arrowIcon').removeClass('fa-flip-horizontal');
// Set panel content opacity
//hidingPanel.find('.hidingPanelContent').css('opacity', '0');
hidingPanel.find('.hidingPanelContent').css({
transform: 'translate(-130px)'
});
},
'click button.btnPin': function(e, template) {
var isPinned = template.isPinned.get();
isPinned = !isPinned;
template.isPinned.set(isPinned);
// Set isPanelPinned of ViewerData
var contentId = template.data.contentId;
ViewerData[contentId].isPanelPinned = isPinned;
Session.set('ViewerData', ViewerData);
if (isPinned) {
pinPanel();
} else {
unpinPanel();
}
resizeViewportElements();
}
});
Template.hidingPanel.helpers({
studyDateIsShown: function() {
return true;
},
isPinned: function() {
return Template.instance().isPinned.get();
}
});
Template.hidingPanel.onRendered(function() {
var contentId = this.data.contentId;
var isPanelPinned = ViewerData[contentId].isPanelPinned;
if (isPanelPinned) {
pinPanel();
}
});
Template.hidingPanel.onCreated(function() {
this.panelOpen = new ReactiveVar(false);
var contentId = this.data.contentId;
var isPanelPinned = ViewerData[contentId].isPanelPinned;
if (!isPanelPinned) {
isPanelPinned = false;
}
this.isPinned = new ReactiveVar(isPanelPinned);
});
@@ -1,46 +0,0 @@
.hidingPanel
width: 1%
height: 100%
position: absolute
left: 0
top: 70px
bottom: 0
z-index: 10
background-color: black
border: 1px solid #666
&.pinned
border: 1px solid transparent
.iconContainer
width: 100%
height: 35px
overflow: hidden
.arrowIcon
color: white
margin: 0px 3px
font-size: 15pt
line-height: 35px
.btnPin
float: right
margin: 3px
outline: none
.hidingPanelContent
position: absolute
left: -130px
width: 100%
height: 100%
margin: 1px auto
.studyBrowserContainer
height: calc(100% - 150px)
.hidingPanel, .arrowIcon
-o-transition: all 0.3s ease-out
-ms-transition: all 0.3s ease-out
-moz-transition: all 0.3s ease-out
-webkit-transition: all 0.3s ease-out
transition: all 0.3s ease-out
@@ -1,33 +0,0 @@
<template name="layout">
<nav class="navbar navbar-default noselect" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">
<img height="30px" src="/images/logo.png">
<h4 class="name">Open Health Imaging Foundation</h4>
</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li>
<a href="http://www.ohif.org">About</a>
</li>
<li>
<form class="navbar-form">
<div class="form-group">
{{> loginButtons align="right"}}
</div>
</form>
</li>
</ul>
</div>
</div>
</nav>
{{> yield}}
</template>
@@ -1,47 +0,0 @@
.noselect
-webkit-touch-callout: none
-webkit-user-select: none
-khtml-user-select: none
-moz-user-select: none
-ms-user-select: none
user-select: none
.navbar
margin-bottom: 0
border-radius: 0
color: #888888
background-color: #060606
border: none
font-family: "Sanchez"
color: #C1C1C1
.navbar-default .navbar-nav > li > a
color: #C1C1C1
font-weight: bold
font-size: 12pt
.navbar-form
height: 50px
margin: 0
padding: 15px 3px
font-weight: bold
font-size: 12pt
line-height: 20px
.navbar-brand
display: inline-block
img
float: left
margin-right: 10px
margin-top: -5px
h4.name
float: left
margin: 0
font-family: "Sanchez"
color: #C1C1C1
@media (min-width: 768px)
.navbar
border-radius: 0px
@@ -1,3 +0,0 @@
<template name="login">
{{> atForm}}
</template>
@@ -1,3 +0,0 @@
<template name="notFound">
Not found
</template>
@@ -1,17 +0,0 @@
<template name="progressDialog">
<div id="progressDialog" tabindex="0">
<div class="dialogHeader">
<h5>{{progressDialogTitle}}</h5>
</div>
<div class="dialogContent">
<div class="progress">
<div class="progress-bar progress-bar-striped active" role="progressbar" style="width:{{progressStatus}}">
{{progressStatus}}
</div>
</div>
<div id="message">
{{{progressMessage}}}
</div>
</div>
</div>
</template>
@@ -1,100 +0,0 @@
progressDialog = {
/**
* Shows progress dialog
* @param {Object} settings - Settings used by progress dialog
* @param {string} settings.title
* @param {int} settings.numberOfCompleted - The value of progress dialog
* @param {int} settings.numberOfTotal - The max value of progress dialog
* @param {Object} [settings.messageParams] - Show a message in progress dialog by setting message and messageType
* @param {string} [settings.messageParams.message="Progress"] - The message will be shown in the progress dialog
* @param {string} [settings.messageParams.messageType="info"] - Sets ui, accepts bootstrap predefined classes such as success, info, warning, danger
*/
'show': function(settings) {
// Set dialog settings
Session.set("progressDialogSettings", settings);
$('#progressDialog').css('display', 'block');
},
/**
* Updates the value of the progress dialog
* @param {int} numberOfCompleted
*/
'update': function(numberOfCompleted) {
var progressDialogSettings = Session.get("progressDialogSettings");
progressDialogSettings.numberOfCompleted = numberOfCompleted;
Session.set("progressDialogSettings", progressDialogSettings);
if (progressDialogSettings.numberOfCompleted === progressDialogSettings.numberOfTotal) {
progressDialog.close();
}
},
/**
* Closes the progress dialog
*/
'close': function() {
// Reset progressDialogSettings session
resetDialogSettingsSession();
// Close dialog
$('#progressDialog').css('display', 'none');
},
/**
* Shows a message in the progress dialog
* @param {Object} messageParams
* @param {string} messageParams.message
* @param {string} messageParams.messageType
*/
'setMessage': function(messageParams) {
var progressDialogSettings = Session.get("progressDialogSettings");
if (!messageParams.messageType || messageParams.messageType == '') {
messageParams.messageType = 'info';
}
progressDialogSettings.messageParams = messageParams;
Session.set("progressDialogSettings", progressDialogSettings);
}
};
Template.progressDialog.helpers({
'progressDialogTitle': function () {
if (Session.get("progressDialogSettings") && Session.get("progressDialogSettings").title) {
return Session.get("progressDialogSettings").title;
}
return "Progress:";
},
'progressStatus': function() {
var numberOfCompleted = 0;
if (Session.get("progressDialogSettings") && Session.get("progressDialogSettings").numberOfCompleted) {
numberOfCompleted = Session.get("progressDialogSettings").numberOfCompleted;
}
var numberofTotal = 1;
if (Session.get("progressDialogSettings") && Session.get("progressDialogSettings").numberOfTotal) {
numberofTotal = Session.get("progressDialogSettings").numberOfTotal;
}
return parseInt((numberOfCompleted / numberofTotal) * 100) + "%";
},
'progressMessage': function() {
var progressDialogSettings = Session.get("progressDialogSettings");
var messageParams = progressDialogSettings && progressDialogSettings.messageParams || false;
if (messageParams && messageParams.message) {
return '<span class="label label-'+messageParams.messageType+'">'+messageParams.message+'</span>';
}
return;
}
});
// Resets progressDialogSettings
function resetDialogSettingsSession() {
Session.set("progressDialogSettings",
{
title: 'Progress',
numberOfCompleted: 0,
numberOfTotal: 0,
messageParams: {message: null, messageType: 'info'}
}
);
}
@@ -1,5 +0,0 @@
<template name="studyNotFound">
<div class="studyNotFound">
<h5>Study not found</h5>
</div>
</template>
@@ -1,6 +0,0 @@
.studyNotFound
margin: 30px
h5
text-align: center
color: #888888
@@ -0,0 +1,11 @@
<template name="relatedStudySelect">
<div class="relatedStudySelect">
<label>Study:</label>
<span class="loading"><i class="fa fa-spinner fa-spin"></i></span>
<select id="selectStudyDate">
{{#each relatedStudies}}
<option value="{{studyInstanceUid}}" selected={{selected}}>{{formatDA studyDate}}: {{studyDescription}}</option>
{{/each}}
</select>
</div>
</template>
@@ -0,0 +1,105 @@
Template.relatedStudySelect.helpers({
/**
* Returns an array of studies that are related to the current study by patient ID.
* The value for 'selected' for the currently loaded study is set to true, so that
* this becomes the current option in the combo box.
*
* @returns {*} Array of studies that are related to the current study by patient ID
*/
relatedStudies: function() {
// Check which study is currently loaded into the study browser
var currentStudyInBrowser = ViewerStudies.findOne({
selected: true
});
// Find all studies in the Worklist which have the same patientId as the currently selected study
var relatedStudies = WorklistStudies.find({
patientId: currentStudyInBrowser.patientId
}, {
sort: {
studyDate: -1
}
}).fetch();
// If no Study / Timepoint associated studies exist, just
// return the list of loaded studies
if (!relatedStudies.length) {
return ViewerStudies.find();
}
// Modify the array of related studies so the default option is the currently selected study
relatedStudies.forEach(function(study, index) {
// If the studyInstanceUid matches that of the current study in the browser,
// Set this to 'selected', so that it becomes the default option
if (study.studyInstanceUid === currentStudyInBrowser.studyInstanceUid) {
relatedStudies[index].selected = true;
}
});
// Use this array to populate the combo box
return relatedStudies;
}
});
Template.relatedStudySelect.events({
/**
* When the study date selector combo box is changed, we will
* hide the select box, temporarily display a loading sign, and grab
* the selected study. Once the study has been retrieved it is added
* into the ViewerStudies collection and set as selected. This reactively
* populated the thumbnail browser.
*
* @param e The select box change event
*/
'change select#selectStudyDate': function(e) {
var selectBox = $(e.currentTarget);
var studyInstanceUid = selectBox.val();
// Hide the select box
selectBox.css('display', 'none');
// Show the loading indicator
var loadingIndicator = selectBox.siblings('.loading');
loadingIndicator.css('display', 'block');
getStudyMetadata(studyInstanceUid, function(study) {
sortStudy(study);
// Hide the loading indicator
loadingIndicator.css('display', 'none');
// Show the select box again
selectBox.css('display', 'block');
// Set "Selected" to false for the entire collection
ViewerStudies.update({}, {
$set: {
selected: false
}
}, {
multi: true
});
// Check if this study already exists in the ViewerStudies collection
// of loaded studies. If it does, set it's 'selected' value to true.
var existingStudy = ViewerStudies.findOne({
studyInstanceUid: studyInstanceUid
});
if (existingStudy) {
// Set the current finding in the collection to true
ViewerStudies.update(existingStudy._id, {
$set: {
selected: true
}
});
return;
}
// If the study does not exist, add the 'selected' key to the object
// with the value True, and insert it into the ViewerStudies Collection
study.selected = true;
ViewerStudies.insert(study);
});
}
});
@@ -0,0 +1,23 @@
.relatedStudySelect
margin: 0 auto 15px
text-align: center
width: 100px
#selectStudyDate
color: black
background-color: #666
border-color: #666
width: 96%
margin: 0 auto
label
font-weight: normal
color: #666
float: left
padding-left: 2%
.loading
display: none
color: white
text-align: center
@@ -2,7 +2,9 @@
<div class="studyBrowser">
<div class="scrollableStudyThumbnails">
{{ #each studies }}
{{ >thumbnails }}
{{#each thumbnails}}
{{ >thumbnailEntry }}
{{/each}}
{{ /each }}
</div>
</div>
@@ -1,5 +1,18 @@
Template.studyBrowser.helpers({
studies : function() {
return ViewerStudies.find({selected: true});
},
thumbnails: function() {
var study = this;
var stacks = createStacks(study);
var array = [];
stacks.forEach(function(stack, index) {
array.push({
thumbnailIndex: index,
stack: stack
});
});
return array;
}
});
@@ -184,9 +184,10 @@ function thumbnailDragEndHandler(e, target) {
return false;
}
var viewportIndex = $('.imageViewerViewport').index(element);
// Rerender the viewport using the drag and drop data
rerenderViewportWithNewSeries(element, OHIF.viewer.dragAndDropData);
Session.get('UseHangingProtocol', false);
layoutManager.rerenderViewportWithNewSeries(viewportIndex, OHIF.viewer.dragAndDropData);
return false;
}
@@ -1,5 +0,0 @@
<template name="thumbnails">
{{#each thumbnails}}
{{ >thumbnailEntry }}
{{/each}}
</template>
@@ -1,15 +0,0 @@
Template.thumbnails.helpers({
thumbnails: function() {
var study = this;
var stacks = createStacks(study);
var array = [];
stacks.forEach(function(stack, index) {
array.push({
thumbnailIndex: index,
stack: stack
});
});
return array;
}
});
@@ -19,7 +19,6 @@ function updateFramerate(rate) {
// If the movie is playing, stop/start to update the framerate
if (isPlaying()) {
var element = getActiveViewportElement();
cornerstoneTools.stopClip(element);
cornerstoneTools.playClip(element);
}
@@ -1,6 +1,6 @@
<template name="imageViewerViewports">
<template name="gridLayout">
<div id='imageViewerViewports'>
{{ #each viewerWindow }}
{{ #each viewports }}
<div class="viewportContainer" style="height:{{height}}%;width:{{width}}%;">
<div class="removable">
{{ >imageViewerViewport}}
@@ -0,0 +1,38 @@
Template.gridLayout.helpers({
height: function() {
var rows = this.rows || 1;
return 100 / rows;
},
width: function() {
var columns = this.columns || 1;
return 100 / columns;
},
viewports: function() {
var numViewports = this.rows * this.columns;
var viewportData = this.viewportData;
var numViewportsWithData = this.viewportData.length;
if (numViewportsWithData < numViewports) {
var difference = numViewports - numViewportsWithData;
for (var i = 0; i < difference; i++) {
viewportData.push({
viewportIndex: numViewportsWithData + i + 1,
rows: this.rows,
columns: this.columns
});
}
} else if (numViewportsWithData > numViewports) {
return viewportData.slice(0, numViewports);
}
return viewportData;
}
});
Template.gridLayout.onRendered(function() {
OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer('CornerstoneNewImage', cornerstoneTools.updateImageSynchronizer);
});
Template.gridLayout.onDestroyed(function() {
OHIF.viewer.updateImageSynchronizer.destroy();
});
@@ -1,7 +1,7 @@
#imageViewerViewports
height: calc(100% - 30px)
height: 100%
width: 100%
.viewportContainer
float: left
border: 2px solid #777
@@ -1,20 +0,0 @@
<template name="hangingProtocolButtons">
<button id="previousPresentationGroup" type="button"
class="imageViewerCommand btn btn-sm btn-default"
data-container="body"
data-toggle="tooltip"
data-placement="bottom"
title="Previous presentation group"
disabled="{{ #unless isPreviousAvailable }} true {{ /unless }}">
<span class="fa fa-step-backward"></span>
</button>
<button id="nextPresentationGroup" type="button"
class="imageViewerCommand btn btn-sm btn-default"
data-container="body"
data-toggle="tooltip"
data-placement="bottom"
title="Next presentation group"
disabled="{{ #unless isNextAvailable }} true {{ /unless }}">
<span class="fa fa-step-forward"></span>
</button>
</template>
@@ -1,11 +0,0 @@
Template.hangingProtocolButtons.helpers({
isNextAvailable: function() {
var presentationGroup = Session.get('WindowManagerPresentationGroup');
var numPresentationGroups = WindowManager.getNumPresentationGroups();
return presentationGroup < numPresentationGroups;
},
isPreviousAvailable: function() {
var presentationGroup = Session.get('WindowManagerPresentationGroup');
return presentationGroup > 1;
}
});
@@ -21,6 +21,7 @@ function loadSeriesIntoViewport(data, templateData) {
// The viewport index is often used to store information about a viewport element
var element = data.element;
var viewportIndex = $('.imageViewerViewport').index(element);
layoutManager.viewportData[viewportIndex].viewportIndex = viewportIndex;
// Get the contentID of the current worklist tab, if the viewport is running
// alongside the worklist package
@@ -140,15 +141,47 @@ function loadSeriesIntoViewport(data, templateData) {
return;
}
// If there is a saved object containing Cornerstone viewport data
// (e.g. scale, invert, window settings) in the input data, apply it now.
//
// Otherwise, display the loaded image in the viewport element with the
// default viewport settings.
if (data.viewport) {
// Update the enabled element with the image and viewport data
// This is not usually necessary, but we need them stored in case
// a sopClassUid-specific viewport setting is present.
enabledElement.image = image;
enabledElement.viewport = cornerstone.getDefaultViewport(enabledElement.canvas, image);
// Check if there are default viewport settings for this sopClassUid
if (!series.instances || !series.instances.length) {
return;
}
var instance = series.instances[0];
var instanceClassViewport = getInstanceClassDefaultViewport(instance, enabledElement, image.imageId);
// If there are sopClassUid-specific viewport settings, apply them
if (instanceClassViewport) {
cornerstone.displayImage(element, image, instanceClassViewport);
// Mark that this element should not be fit to the window in the resize listeners
// TODO: Find another way to do this?
enabledElement.fitToWindow = false;
// Resize the canvas to fit the current viewport element size.
cornerstone.resize(element, false);
} else if (data.viewport) {
// If there is a saved object containing Cornerstone viewport data
// (e.g. scale, invert, window settings) in the input data, apply it now.
cornerstone.displayImage(element, image, data.viewport);
// Resize the canvas to fit the current viewport element size. Fit the displayed
// image to the canvas dimensions.
cornerstone.resize(element, true);
} else {
// If no saved viewport settings or modality-specific settings exists,
// display the loaded image in the viewport element with no loaded viewport
// settings.
cornerstone.displayImage(element, image);
// Resize the canvas to fit the current viewport element size. Fit the displayed
// image to the canvas dimensions.
cornerstone.resize(element, true);
}
// Remove the data for this viewport from the ViewportLoading object
@@ -159,10 +192,6 @@ function loadSeriesIntoViewport(data, templateData) {
// (e.g. hide the progress text box)
endLoadingHandler(element);
// Resize the canvas to fit the current viewport element size. Fit the displayed
// image to the canvas dimensions.
cornerstone.resize(element, true);
// Remove the 'empty' class from the viewport to hide any instruction text
element.classList.remove('empty');
@@ -172,7 +201,7 @@ function loadSeriesIntoViewport(data, templateData) {
$(element).siblings('.imageViewerViewportOverlay').show();
// Add stack state managers for the stack tool, CINE tool, and reference lines
cornerstoneTools.addStackStateManager(element, [ 'stack', 'playClip', 'referenceLines' ]);
cornerstoneTools.addStackStateManager(element, ['stack', 'playClip', 'referenceLines']);
// Get the current viewport settings
var viewport = cornerstone.getViewport(element);
@@ -206,7 +235,7 @@ function loadSeriesIntoViewport(data, templateData) {
// Use the tool manager to enable the currently active tool for this
// newly rendered element
var activeTool = toolManager.getActiveTool();
toolManager.setActiveTool(activeTool, [ element ]);
toolManager.setActiveTool(activeTool, [element]);
// Define a function to run whenever the Cornerstone viewport is rendered
// (e.g. following a change of window or zoom)
@@ -220,6 +249,7 @@ function loadSeriesIntoViewport(data, templateData) {
// Save the current viewport into the ViewerData global variable, as well as the
// Meteor Session. This lets the viewport be saved/reloaded on a hot-code reload
var viewport = cornerstone.getViewport(element);
layoutManager.viewportData[viewportIndex].viewport = viewport;
ViewerData[contentId].loadedSeriesData[viewportIndex].viewport = viewport;
Session.set('ViewerData', ViewerData);
}
@@ -240,6 +270,7 @@ function loadSeriesIntoViewport(data, templateData) {
// This allows the template helpers to update reactively
templateData.imageId = eventData.enabledElement.image.imageId;
Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
layoutManager.viewportData[viewportIndex].imageId = eventData.enabledElement.image.imageId;
// Get the element and stack data
var element = e.target;
@@ -260,6 +291,7 @@ function loadSeriesIntoViewport(data, templateData) {
var stack = cornerstoneTools.getToolState(element, 'stack');
if (stack && stack.data.length && stack.data[0].imageIds.length > 1) {
var imageIdIndex = stack.data[0].imageIds.indexOf(templateData.imageId);
layoutManager.viewportData[viewportIndex].currentImageIdIndex = imageIdIndex;
ViewerData[contentId].loadedSeriesData[viewportIndex].currentImageIdIndex = imageIdIndex;
Session.set('ViewerData', ViewerData);
}
@@ -327,14 +359,17 @@ function loadSeriesIntoViewport(data, templateData) {
$(element).off(allCornerstoneEvents, sendActivationTrigger);
$(element).on(allCornerstoneEvents, sendActivationTrigger);
// Store the current series data inside a global variable
OHIF.viewer.loadedSeriesData[viewportIndex] = {
// Store the current series data inside the Layout Manager
layoutManager.viewportData[viewportIndex] = {
imageId: imageId,
studyInstanceUid: data.studyInstanceUid,
seriesInstanceUid: data.seriesInstanceUid,
currentImageIdIndex: data.currentImageIdIndex,
viewport: viewport
viewport: viewport,
viewportIndex: viewportIndex
};
ViewerData[contentId].loadedSeriesData = OHIF.viewer.loadedSeriesData;
ViewerData[contentId].loadedSeriesData = layoutManager.viewportData;
// Check if image plane (orientation / loction) data is present for the current image
var imagePlane = cornerstoneTools.metaData.get('imagePlane', image.imageId);
@@ -443,7 +478,7 @@ Meteor.startup(function() {
});
Template.imageViewerViewport.onCreated(function() {
log.info('imageViewerViewport onCreated');
log.info('imageViewerViewport onCreated');
});
Template.imageViewerViewport.onRendered(function() {
@@ -487,23 +522,6 @@ Template.imageViewerViewport.onRendered(function() {
});
var seriesInstanceUid = this.data.seriesInstanceUid;
// TODO: This code block might be refactored
// Load previous measurement study when reloading a patient
if (!study) {
getStudyMetadata(this.data.studyInstanceUid, function(study) {
// Once we have retrieved the data, we sort the series' by series
// and instance number in ascending order
if (!study) {
return;
}
sortStudy(study);
data.study = study;
setSeries(data, seriesInstanceUid, templateData);
})
}
data.study = study;
setSeries(data, seriesInstanceUid, templateData);
});
@@ -539,5 +557,9 @@ Template.imageViewerViewport.events({
'click .imageViewerViewport': function(e) {
var viewportIndex = $('.imageViewerViewport').index(e.currentTarget);
Session.set('activeViewport', viewportIndex);
},
'dblclick .imageViewerViewport': function(e) {
var viewportIndex = $('.imageViewerViewport').index(e.currentTarget);
layoutManager.toggleEnlargement(viewportIndex);
}
});
});
@@ -1,201 +0,0 @@
ViewerWindows = new Meteor.Collection(null);
ViewerWindows._debugName = 'ViewerWindows';
Template.imageViewerViewports.helpers({
height: function() {
var viewportRows = this.viewportRows || 1;
return 100 / viewportRows;
},
width: function() {
var viewportColumns = this.viewportColumns || 1;
return 100 / viewportColumns;
},
viewerWindow: function() {
log.info('ViewerWindows');
//log.info(ViewerWindows.find().fetch());
ViewerWindows.remove({});
log.info('imageViewerViewports viewportArray');
var viewportRows = this.viewportRows || 1;
var viewportColumns = this.viewportColumns || 1;
var contentId = Session.get('activeContentId');
if (this.viewportRows && this.viewportColumns) {
viewportRows = this.viewportRows || 1;
viewportColumns = this.viewportColumns || 1;
} else if (ViewerData[contentId].viewportRows && ViewerData[contentId].viewportColumns) {
viewportRows = ViewerData[contentId].viewportRows;
viewportColumns = ViewerData[contentId].viewportColumns;
}
var viewportData;
if (!$.isEmptyObject(ViewerData[contentId].loadedSeriesData)) {
viewportData = ViewerData[contentId].loadedSeriesData;
}
var inputData = {
viewportColumns: viewportColumns,
viewportRows: viewportRows
};
inputData.DisplaySetPresentationGroup = Session.get('WindowManagerPresentationGroup');
var hangingProtocolViewportData = WindowManager.getHangingProtocol(inputData);
if (Session.get('UseHangingProtocol')) {
viewportData = hangingProtocolViewportData.viewports;
viewportRows = hangingProtocolViewportData.viewportRows || viewportRows;
viewportColumns = hangingProtocolViewportData.viewportColumns || viewportColumns;
}
// Update viewerData
ViewerData[contentId].viewportRows = viewportRows;
ViewerData[contentId].viewportColumns = viewportColumns;
Session.set('ViewerData', ViewerData);
this.viewportRows = viewportRows;
this.viewportColumns = viewportColumns;
var numViewports = viewportRows * viewportColumns;
for (var i = 0; i < numViewports; ++i) {
var data = {
viewportIndex: i,
// These two are necessary because otherwise the width and height helpers
// don't get the right data context. Seems to be related to the "each" loop.
viewportColumns: viewportColumns,
viewportRows: viewportRows
};
if (viewportData && !$.isEmptyObject(viewportData[i])) {
data.seriesInstanceUid = viewportData[i].seriesInstanceUid;
data.studyInstanceUid = viewportData[i].studyInstanceUid;
data.currentImageIdIndex = viewportData[i].currentImageIdIndex;
data.viewport = viewportData[i].viewport;
} else if (hangingProtocolViewportData && !$.isEmptyObject(hangingProtocolViewportData.viewports[i])) {
data.seriesInstanceUid = hangingProtocolViewportData.viewports[i].seriesInstanceUid;
data.studyInstanceUid = hangingProtocolViewportData.viewports[i].studyInstanceUid;
data.currentImageIdIndex = hangingProtocolViewportData.viewports[i].currentImageIdIndex;
data.viewport = hangingProtocolViewportData.viewports[i].viewport;
}
ViewerWindows.insert(data);
}
// Here we will find out if we need to load any other studies into the viewer
// We will make a list of unique studyInstanceUids
var uniqueStudyInstanceUids = [];
// Meteor doesn't support Mongo's 'distinct' function, so we have to do this in a loop
var windows = ViewerWindows.find({}, {
reactive: false
}).fetch();
windows.forEach(function(window) {
var studyInstanceUid = window.studyInstanceUid;
if (!studyInstanceUid) {
return;
}
// If this studyInstanceUid is already in the list, stop here
if (uniqueStudyInstanceUids.indexOf(studyInstanceUid) > -1) {
return;
}
// Otherwise, add it to the list
uniqueStudyInstanceUids.push(studyInstanceUid);
// If any of the associated studies is not already loaded, load it now
var loadedStudy = ViewerStudies.findOne({
studyInstanceUid: studyInstanceUid
}, {
reactive: false
});
if (!loadedStudy) {
// Load the study
getStudyMetadata(studyInstanceUid, function(study) {
log.info("imageViewerViewports GetStudyMetadata: " + studyInstanceUid);
// Sort the study's series and instances by series and instance number
sortStudy(study);
// Insert it into the ViewerStudies Collection
ViewerStudies.insert(study);
});
}
});
return ViewerWindows.find({}, {
reactive: false
}).fetch();
}
});
var savedSeriesData,
savedViewportRows,
savedViewportColumns;
Template.imageViewerViewports.events({
'CornerstoneMouseDoubleClick .imageViewerViewport': function(e) {
var container = $('.viewerMain').get(0);
var data;
var contentId = this.contentId || $('#viewer').parents('.tab-pane.active').attr('id');
// If there is more than one viewport on screen
// And one of them is double-clicked, it should be rendered alone
// If it is double-clicked again, the viewer should revert to the previous layout
if ($(e.currentTarget).hasClass('zoomed')) {
// Revert to saved settings
ViewerData[contentId].loadedSeriesData = $.extend(true, {}, savedSeriesData);
ViewerData[contentId].viewportRows = savedViewportRows;
ViewerData[contentId].viewportColumns = savedViewportColumns;
savedViewportRows = 0;
savedViewportColumns = 0;
data = {
viewportRows: ViewerData[contentId].viewportRows,
viewportColumns: ViewerData[contentId].viewportColumns
};
// Render the imageViewerViewports template with these settings
$('#imageViewerViewports').remove();
UI.renderWithData(Template.imageViewerViewports, data, container);
// Remove the 'zoomed' class from any viewports
$('.imageViewerViewport').removeClass('zoomed');
} else {
// Zoom to single viewport
// If only one viewport is on-screen, stop here
if (ViewerData[contentId].viewportRows === 1 &&
ViewerData[contentId].viewportColumns === 1) {
return;
}
// Save the current settings
savedSeriesData = $.extend(true, {}, ViewerData[contentId].loadedSeriesData);
savedViewportRows = ViewerData[contentId].viewportRows;
savedViewportColumns = ViewerData[contentId].viewportColumns;
// Get the clicked-on viewport's index
var viewportIndex = this.viewportIndex;
// Set the first viewport's data to be the same as the currently clicked-on viewport
ViewerData[contentId].loadedSeriesData[0] = ViewerData[contentId].loadedSeriesData[viewportIndex];
// Set the basic template data
data = {
viewportRows: 1,
viewportColumns: 1
};
// Render the imageViewerViewports template with these settings
$('#imageViewerViewports').remove();
UI.renderWithData(Template.imageViewerViewports, data, container);
// Add the 'zoomed' class to the lone remaining viewport
$('.imageViewerViewport').eq(0).addClass('zoomed');
}
}
});
@@ -1,6 +0,0 @@
<template name="layoutButton">
<button id="layout" type="button" class="btn btn-sm btn-default dropdown-toggle" data-container="body" data-toggle="dropdown" aria-expanded="false" data-placement="right" title="Layout" rel="tooltip">
<span class="fa fa-th-large"></span>
</button>
{{ >layoutChooser }}
</template>
@@ -1,20 +1,29 @@
<template name="layoutChooser">
<div class="layoutChooser pull-right dropdown-menu" role="menu">
<div class="layoutChooser pull-left dropdown-menu" role="menu">
<table>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</table>
</div>
@@ -33,19 +33,18 @@ Template.layoutChooser.events({
highlightCells(evt.currentTarget);
},
'click .layoutChooser table td': function(evt) {
$('#imageViewerViewports').remove();
var currentCell = $(evt.currentTarget);
var rowIndex = currentCell.closest('tr').index();
var columnIndex = currentCell.index();
// Add 1 because the indices start from zero
var data = {
viewportRows: rowIndex + 1,
viewportColumns: columnIndex + 1,
var layoutProps = {
rows: rowIndex + 1,
columns: columnIndex + 1
};
var container = $(".viewerMain").get(0);
UI.renderWithData(Template.imageViewerViewports, data, container);
layoutManager.layoutTemplateName = 'gridLayout';
layoutManager.layoutProps = layoutProps;
layoutManager.updateViewports();
}
});
@@ -1,7 +1,8 @@
.layoutChooser
width: 75px
min-width: 75px // to override bootstrap's dropdown-menu class
height: 75px
width: 92px
height: 92px
min-width: 92px // to override bootstrap's dropdown-menu class
min-height: 92px
table
margin: 0 auto
@@ -11,7 +12,7 @@
border: 2px solid black
width: 20px
height: 20px
transition: background-color 0.1s linear
transition: background-color 0.1s ease
&:hover, &.hover // Add the hover class here to be triggered by mouseenter/mouseleave
background-color: gray
@@ -10,11 +10,17 @@
{{ /if }}
{{ #if includeLayoutButton }}
{{ >layoutButton }}
<div class="btn-group">
<button id="layout" type="button" class="btn btn-sm btn-default dropdown-toggle" data-container="body" data-toggle="dropdown" aria-expanded="false" data-placement="right" title="Layout" rel="tooltip">
<span class="fa fa-th-large"></span>
</button>
{{ >layoutChooser }}
</div>
{{ /if }}
{{ #if includeHangingProtocolButtons }}
{{ >hangingProtocolButtons }}
{{ >matchedProtocols }}
{{ /if }}
{{ #each btnGroup }}
@@ -152,7 +152,7 @@ Template.toolbar.onRendered(function() {
var states = toolManager.getToolDefaultStates();
var disabledToolButtons = states.disabledToolButtons;
var allToolbarButtons = $('#toolbar').find('button');
if (disabledToolButtons.length > 0) {
if (disabledToolButtons && disabledToolButtons.length > 0) {
for (var i = 0; i < allToolbarButtons.length; i++) {
var toolbarButton = allToolbarButtons[i];
$(toolbarButton).prop('disabled', false);
@@ -191,7 +191,7 @@ Template.toolbar.helpers({
return this.toolbarOptions.includeHangingProtocolButtons;
}
return false;
return true;
},
btnGroup: function() {
if (this.toolbarOptions && this.toolbarOptions.btnGroup !== undefined) {
@@ -22,7 +22,7 @@ updateOrientationMarkers = function(element) {
var markers = {
top: oppositeColumnString,
left: oppositeRowString,
left: oppositeRowString
};
// If any vertical or horizontal flips are applied, change the orientation strings ahead of
@@ -30,12 +30,13 @@ updateOrientationMarkers = function(element) {
if (viewport.vflip) {
markers.top = cornerstoneTools.orientation.invertOrientationString(markers.top);
}
if (viewport.hflip) {
markers.left = cornerstoneTools.orientation.invertOrientationString(markers.left);
}
// Get the viewport orientation marker DOM elements
var viewportOrientationMarkers = $(element).siblings(".viewportOrientationMarkers");
var viewportOrientationMarkers = $(element).siblings('.viewportOrientationMarkers');
var topMarker = viewportOrientationMarkers.find('.topMid');
var leftMarker = viewportOrientationMarkers.find('.leftMid');
@@ -54,4 +55,4 @@ updateOrientationMarkers = function(element) {
topMarker.text(markers.top);
leftMarker.text(markers.left);
}
};
};
@@ -1,5 +1,6 @@
<template name="viewportOverlay">
<div class="imageViewerViewportOverlay noselect">
{{ #unless tagDisplaySpecified }}
<div class="topleft dicomTag">
<div>{{formatPN patientName}}</div>
<div>{{patientId}}</div>
@@ -21,6 +22,47 @@
<div>{{imageDimensions}}</div>
<div>{{seriesDescription}}</div>
</div>
{{ /unless }}
{{ #if tagDisplayRightOnly }}
<div class="topright dicomTag">
<div>{{formatPN patientName}}</div>
<div>{{patientId}}</div>
<div class='priorIndicator'>{{prior}}</div>
<div>{{studyDescription}}</div>
<div>{{formatDA studyDate}} {{formatTM studyTime}}</div>
</div>
<div class="bottomright dicomTag">
<div>Ser: {{seriesNumber}}</div>
<div>Img: {{imageNumber}} ({{imageIndex}}/{{numImages}})</div>
<div>{{#if frameRate}}{{frameRate}} FPS{{/if}}</div>
<div>{{imageDimensions}}</div>
<div>{{seriesDescription}}</div>
<div>Zoom: {{formatNumberPrecision zoom 2}}%</div>
<div>{{compression}}</div>
<div>{{wwwc}}</div>
</div>
{{ /if }}
{{ #if tagDisplayLeftOnly }}
<div class="topleft dicomTag">
<div>{{formatPN patientName}}</div>
<div>{{patientId}}</div>
<div class='priorIndicator'>{{prior}}</div>
<div>{{studyDescription}}</div>
<div>{{formatDA studyDate}} {{formatTM studyTime}}</div>
</div>
<div class="bottomleft dicomTag">
<div>Ser: {{seriesNumber}}</div>
<div>Img: {{imageNumber}} ({{imageIndex}}/{{numImages}})</div>
<div>{{#if frameRate}}{{frameRate}} FPS{{/if}}</div>
<div>{{imageDimensions}}</div>
<div>{{seriesDescription}}</div>
<div>Zoom: {{formatNumberPrecision zoom 2}}%</div>
<div>{{compression}}</div>
<div>{{wwwc}}</div>
</div>
{{ /if }}
{{>imageControls}}
</div>
</template>
@@ -80,6 +80,20 @@ function getInstance(property) {
return instance[property];
}
function getTagDisplay(property) {
Session.get('CornerstoneNewImage' + this.viewportIndex);
if (!this.imageId) {
return false;
}
var instance = cornerstoneTools.metaData.get('tagDisplay', this.imageId);
if (!instance) {
return '';
}
return instance[property];
}
function getImage(viewportIndex) {
var element = getElementIfNotEmpty(viewportIndex);
if (!element) {
@@ -172,6 +186,15 @@ Template.viewportOverlay.helpers({
imageNumber: function() {
return getInstance.call(this, 'instanceNumber');
},
tagDisplayLeftOnly: function() {
return getTagDisplay.call(this, 'side') === 'L';
},
tagDisplayRightOnly: function() {
return getTagDisplay.call(this, 'side') === 'R';
},
tagDisplaySpecified: function() {
return getTagDisplay.call(this, 'side');
},
imageIndex: function() {
return getInstance.call(this, 'index');
},
@@ -1,5 +1,5 @@
.imageViewerViewportOverlay
font-family: Helvetica, OpenSans
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif
font-size: 13px
color: #979797
line-height: 18px
@@ -1,5 +0,0 @@
/*AccountsTemplates.configure({
forbidClientAccountCreation: true // do not allow account creation
});
*/
@@ -1,18 +0,0 @@
/**
* Converts the properties to URL query parameters. Based on:
* http://stackoverflow.com/questions/111529/create-query-parameters-in-javascript
*
* @param data
* @returns {string}
*/
encodeQueryData = function(data)
{
var ret = [];
for (var d in data) {
if(data[d]) {
ret.push(encodeURIComponent(d) + "=" + encodeURIComponent(data[d]));
}
}
return ret.join("&");
};
-115
View File
@@ -1,115 +0,0 @@
var exportFailed;
/**
* Exports requested studies
* @param studiesToExport Studies to export
*/
exportStudies = function(studiesToExport) {
if (studiesToExport.length < 1) {
return;
}
queryStudies(studiesToExport, exportQueriedStudies);
};
function exportQueriedStudies(studiesToExport) {
var numberOfFilesToExport = 0;
studiesToExport.forEach(function(study) {
numberOfFilesToExport += getNumberOfFilesInStudy(study);
});
progressDialog.show("Exporting Studies...", numberOfFilesToExport);
try {
exportQueriedStudiesInternal(studiesToExport, numberOfFilesToExport);
} catch (err) {
progressDialog.close();
console.error("Failed to export studies: " + err.message);
}
}
function exportQueriedStudiesInternal(studiesToExport, numberOfFilesToExport) {
var zip = new JSZip();
exportFailed = false;
var numberOfFilesExported = 0;
studiesToExport.forEach(function(study) {
sortStudy(study);
var studyFolder = zip.folder(study.studyInstanceUid);
study.seriesList.forEach(function(series) {
var seriesFolder = studyFolder.folder(series.seriesInstanceUid);
series.instances.forEach(function(instance) {
if (!instance.wadouri) {
return;
}
// If failed to download a dicom file, skip others
if (exportFailed) {
return;
}
// Download and Zip the dicom file
var xhr = new XMLHttpRequest();
xhr.open("GET", instance.wadouri, true);
xhr.responseType = "blob";
// Downloaded the dicom file completely
xhr.onload = function(e) {
// If failed to download a dicom file, skip others
if (exportFailed) {
return;
}
// Failed to export a file
if (xhr.readyState === 4 && xhr.status !== 200) {
onExportFailed("File not downloaded: " + instance.wadouri);
return;
}
var blobFile = new Blob([xhr.response], {type: 'application/dicom'});
var fileReader = new FileReader();
fileReader.onload = function() {
try {
seriesFolder.file(instance.sopInstanceUid + ".dcm", fileReader.result, { binary: true });
} catch(err) {
onExportFailed(err.message);
return;
}
numberOfFilesExported++;
if (numberOfFilesExported === numberOfFilesToExport) {
var zipContent = zip.generate({ type: "blob" });
saveAs(zipContent, "studies.zip");
}
progressDialog.update(numberOfFilesExported);
};
fileReader.readAsArrayBuffer(blobFile);
};
// Failed to download the dicom file
xhr.onerror = function() {
onExportFailed("File not downloaded: " + instance.wadouri);
};
xhr.send();
});
});
});
}
function onExportFailed(err) {
exportFailed = true;
progressDialog.close();
//TODO: Export failed and dialog closed, so let user know
console.error("Failed to export studies!", err);
}
@@ -0,0 +1,14 @@
/**
* A global Blaze UI helper to capitalizes the first letter of an input String
*
* Credit to:
*
* http://stackoverflow.com/questions/1026069/capitalize-the-first-letter-of-string-in-javascript
*/
UI.registerHelper('capitalizeFirstLetter', function (context) {
if (!context) {
return;
}
return context.charAt(0).toUpperCase() + context.slice(1);
});
@@ -0,0 +1,16 @@
/**
* A global Blaze UI helper function to format JavaScript Dates using the Moment library
*/
UI.registerHelper('formatJSDate', function(context, format, options) {
if (!context) {
return;
}
var dateAsMoment = moment(new Date(context));
var strFormat = 'MMM D, YYYY';
if (options) {
strFormat = format;
}
return dateAsMoment.format(strFormat);
});
@@ -0,0 +1,12 @@
/**
* Helper for retrieving username given userId
*/
UI.registerHelper('getUsername', function(userId) {
var user = Meteor.users.findOne({
userId: userId
});
if (user && user.name) {
return user.name;
}
});
@@ -0,0 +1,10 @@
/**
* Helper for checking datatype of a variable
*/
UI.registerHelper('ifTypeIs', function(value, match, attributeName) {
if (typeof(value) === match) {
return attributeName;
}
return '';
});
+3
View File
@@ -0,0 +1,3 @@
UI.registerHelper('inc', function(value) {
return parseInt(value) + 1;
});
@@ -0,0 +1,13 @@
/**
* A global Blaze UI helper function to format JavaScript Dates using the Moment library
*/
UI.registerHelper('jsDateFromNow', function(context, format, options) {
if (!context) {
return;
}
Session.get('timeAgoVariable');
var dateAsMoment = moment(new Date(context));
return dateAsMoment.fromNow();
});
@@ -0,0 +1,6 @@
UI.registerHelper('objectEach', function(object) {
// http://stackoverflow.com/questions/30234732/how-to-print-key-and-values-in-meteor-template
return _.map(object, function(value, key) {
return _.extend({key: key}, value);
});
});
@@ -0,0 +1,9 @@
UI.registerHelper('objectToPairs', function(object) {
// http://stackoverflow.com/questions/30234732/how-to-print-key-and-values-in-meteor-template
return _.map(object, function(value, key) {
return {
key: key,
value: value
};
});
});
@@ -0,0 +1,19 @@
/**
* A global Blaze UI helper to Stringify a JavaScript object
*
* Credit to:
*
* http://stackoverflow.com/questions/1026069/capitalize-the-first-letter-of-string-in-javascript
*/
UI.registerHelper('prettyPrintStringify', function(context) {
if (!context) {
return;
}
var string = JSON.stringify(context, null, 2);
string = string.replace(/['"]+/g, '');
string = string.replace('{', '');
string = string.replace('}', '');
string = string.replace(',', '\n');
return string;
});
@@ -0,0 +1,14 @@
var instanceClassViewportSettingsFunctions = {};
getInstanceClassDefaultViewport = function(series, enabledElement, imageId) {
var instanceClass = series.sopClassUid;
if (!instanceClassViewportSettingsFunctions[instanceClass]) {
return;
}
return instanceClassViewportSettingsFunctions[instanceClass](series, enabledElement, imageId);
};
setInstanceClassDefaultViewportFunction = function(instanceClass, fn) {
instanceClassViewportSettingsFunctions[instanceClass] = fn;
};
+211
View File
@@ -0,0 +1,211 @@
// Displays Series in Viewports given a Protocol and list of Studies
LayoutManager = class LayoutManager {
constructor(parentNode, studies) {
log.info('LayoutManager');
this.parentNode = parentNode;
this.studies = studies;
this.viewportData = [];
this.layoutTemplateName = 'gridLayout';
this.layoutProps = {
rows: 1,
columns: 1
};
this.viewportData = [];
this.isZoomed = false;
}
getNumberOfViewports() {
return this.layoutProps.rows * this.layoutProps.columns;
}
setDefaultViewportData() {
var numViewports = this.getNumberOfViewports();
var viewportIndex = 0;
var self = this;
this.studies.forEach(function(study) {
study.seriesList.forEach(function(series) {
if (!series.instances.length) {
return;
}
var currentViewportData;
var existingViewportData = self.viewportData[viewportIndex];
if (self.viewportData[viewportIndex]) {
currentViewportData = {
viewportIndex: existingViewportData.viewportIndex,
studyInstanceUid: existingViewportData.studyInstanceUid,
seriesInstanceUid: existingViewportData.seriesInstanceUid,
sopInstanceUid: existingViewportData.sopInstanceUid,
viewport: existingViewportData.viewport,
imageId: existingViewportData.imageId,
currentImageIdIndex: existingViewportData.currentImageIdIndex // TODO Remove this once currentImageIdIndex is removed from imageViewerViewports
};
self.viewportData[viewportIndex] = currentViewportData;
} else {
// 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];
if (!firstInstance.rows || !firstInstance.columns) {
currentViewportData = {};
} else {
currentViewportData = {
viewportIndex: viewportIndex,
studyInstanceUid: study.studyInstanceUid,
seriesInstanceUid: series.seriesInstanceUid,
sopInstanceUid: firstInstance.sopInstanceUid,
currentImageIdIndex: 0 // TODO Remove this once currentImageIdIndex is removed from imageViewerViewports
};
}
self.viewportData.push(currentViewportData);
}
viewportIndex++;
if (viewportIndex === numViewports) {
return false;
}
});
if (viewportIndex === numViewports) {
return false;
}
});
}
updateViewports() {
log.info('updateViewports');
if (!this.viewportData ||
!this.viewportData.length ||
this.viewportData.length !== this.getNumberOfViewports()) {
this.setDefaultViewportData();
}
// imageViewerViewports occasionally needs relevant layout data in order to set
// the element style of the viewport in question
var layoutProps = this.layoutProps;
var data = $.extend({
viewportData: []
}, layoutProps);
this.viewportData.forEach(function(viewportData) {
var viewportDataAndLayoutProps = $.extend(viewportData, layoutProps);
data.viewportData.push(viewportDataAndLayoutProps);
});
var layoutTemplate = Template[this.layoutTemplateName];
$(this.parentNode).html('');
Blaze.renderWithData(layoutTemplate, data, this.parentNode);
Session.set('LayoutManagerUpdated', Random.id());
this.isZoomed = false;
}
/**
* 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.
*
* @param viewportIndex
* @param data An object containing a seriesIndex and studyIndex for a study to load into this viewport
*/
rerenderViewportWithNewSeries(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);
// 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
this.viewportData[viewportIndex] = {
viewportIndex: viewportIndex,
seriesInstanceUid: data.seriesInstanceUid,
studyInstanceUid: data.studyInstanceUid,
renderedCallback: data.renderedCallback,
currentImageIdIndex: 0
};
// Remove the hover styling
element.find('canvas').not('.magnifyTool').removeClass('faded');
// Remove the whole template, add in the new one
var viewportContainer = element.parents('.removable');
var newViewportContainer = document.createElement('div');
newViewportContainer.className = 'removable';
// Remove the parent element of the template
// This is a workaround since otherwise Blaze UI onDestroyed doesn't fire
viewportContainer.remove();
container.appendChild(newViewportContainer);
// Render and insert the template
Blaze.renderWithData(Template.imageViewerViewport, data, newViewportContainer);
Session.set('LayoutManagerUpdated', Random.id());
}
enlargeViewport(viewportIndex) {
log.info('Zooming Into Viewport: ' + viewportIndex);
if (!this.viewportData ||
!this.viewportData.length) {
return;
}
// Clone the array for later
this.previousViewportData = this.viewportData.slice(0);
var singleViewportData = $.extend({}, this.viewportData[viewportIndex]);
singleViewportData.rows = 1;
singleViewportData.columns = 1;
singleViewportData.viewportIndex = 0;
var data = {
viewportData: [singleViewportData],
rows: 1,
columns: 1
};
var layoutTemplate = Template['gridLayout'];
$(this.parentNode).html('');
Blaze.renderWithData(layoutTemplate, data, this.parentNode);
this.isZoomed = true;
this.zoomedViewportIndex = viewportIndex;
this.viewportData = data.viewportData;
Session.set('LayoutManagerUpdated', Random.id());
}
resetPreviousLayout() {
if (!this.isZoomed) {
return;
}
this.previousViewportData[this.zoomedViewportIndex] = $.extend({}, this.viewportData[0]);
this.previousViewportData[this.zoomedViewportIndex].viewportIndex = this.zoomedViewportIndex;
this.viewportData = this.previousViewportData;
this.updateViewports();
}
toggleEnlargement(viewportIndex) {
if (this.isZoomed) {
this.resetPreviousLayout();
} else {
this.enlargeViewport(viewportIndex);
}
}
};
@@ -89,6 +89,21 @@ addMetaData = function(imageId, data) {
metaDataLookup[imageId] = metaData;
};
/**
* Adds a set of metaData to the Cornerstone metaData provider given a specific
* imageId, type, and dataset
*
* @param imageId
* @param type (e.g. series, instance, tagDisplay)
* @param data
*/
addSpecificMetadata = function(imageId, type, data) {
var metaData = {};
metaData[type] = data;
metaDataLookup[imageId] = $.extend(metaDataLookup[imageId], metaData);
};
/**
* Looks up metaData for Cornerstone Tools given a specified type and imageId
* A type may be, e.g. 'study', or 'patient', or 'imagePlane'. These types
@@ -1,48 +0,0 @@
/**
* 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.
*
* @param element
* @param data An object containing a seriesIndex and studyIndex for a study to load into this viewport
* @function renderedCallback An optional callback to be executed after the new template is rendered
*/
rerenderViewportWithNewSeries = function(element, data, renderedCallback) {
// Get the container and index of the current viewport.
// The parent container is identified because it is later removed from the DOM
var container = $(element).parents('.viewportContainer').get(0);
var viewportIndex = $(container).index();
// 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
OHIF.viewer.loadedSeriesData[viewportIndex] = {
seriesInstanceUid: data.seriesInstanceUid,
studyInstanceUid: data.studyInstanceUid,
currentImageIdIndex: 0
};
// Remove the hover styling
$(element).find('canvas').not('.magnifyTool').removeClass("faded");
// Remove the whole template, add in the new one
var viewportContainer = $(element).parents('.removable');
var newViewportContainer = document.createElement('div');
newViewportContainer.className = 'removable';
// Remove the parent element of the template
// This is a workaround since otherwise Blaze UI onDestroyed doesn't fire
viewportContainer.remove();
container.appendChild(newViewportContainer);
// Pass the renderedCallback to the imageViewerViewport template
if (renderedCallback && typeof renderedCallback === "function") {
data.renderedCallback = renderedCallback;
}
// Render and insert the template
UI.renderWithData(Template.imageViewerViewport, data, newViewportContainer);
};
@@ -15,8 +15,22 @@ resizeViewportElements = function() {
viewportResizeTimer = setTimeout(function() {
var elements = $('.imageViewerViewport').not('.empty');
elements.each(function(index, element) {
var enabledElement;
try {
enabledElement = cornerstone.getEnabledElement(element);
} catch(error) {
return;
}
cornerstone.resize(element, true);
if (enabledElement.fitToWindow === false) {
var imageId = enabledElement.image.imageId;
var instance = cornerstoneTools.metaData.get('instance', imageId);
var instanceClassViewport = getInstanceClassDefaultViewport(instance, enabledElement, imageId);
cornerstone.setViewport(element, instanceClassViewport);
}
// TODO= Refactor this into separate scrollbar resize function
var currentOverlay = $(element).siblings('.imageViewerViewportOverlay');
var imageControls = currentOverlay.find('.imageControls');
@@ -0,0 +1,61 @@
setMammogramViewportAlignment = function(series, enabledElement, imageId) {
// Don't apply the MG viewport alignment to other series types
var viewTypes = ['MLO', 'CC', 'LM', 'ML', 'XCCL'];
var requiresInversion = ['LM']; // Tomo series are flipped
var instance = cornerstoneTools.metaData.get('instance', imageId);
if (!instance) {
return;
}
var laterality = instance.laterality;
var element = enabledElement.element;
var position;
var left = $(enabledElement.canvas).offset().left;
var right = left + enabledElement.canvas.width;
if (viewTypes.indexOf(instance.viewPosition) < 0) {
return;
}
// Check if we should flip the laterality
if (requiresInversion.indexOf(instance.viewPosition) > -1) {
if (laterality === "R") {
laterality = "L";
} else if (laterality === "L") {
laterality = "R";
}
}
if (laterality === "R") {
// Set X translation to Canvas max in image pixels - image width
// This places it on the right side of the screen
position = cornerstone.pageToPixel(element, right, 0);
if (position.x !== enabledElement.image.width) {
enabledElement.viewport.translation.x += position.x - enabledElement.image.width;
}
addSpecificMetadata(imageId, 'tagDisplay', {
side: 'L'
});
} else if (laterality === 'L') {
// Use pageToPixel to and jQuery to find pixel coordinates of leftmost
// side of the current canvas
position = cornerstone.pageToPixel(element, left, 0);
if (position.x !== 0) {
enabledElement.viewport.translation.x += position.x;
}
addSpecificMetadata(imageId, 'tagDisplay', {
side: 'R'
});
}
return enabledElement.viewport;
};
Meteor.startup(function() {
setInstanceClassDefaultViewportFunction("1.2.840.10008.5.1.4.1.1.1.2", setMammogramViewportAlignment);
});
+23
View File
@@ -0,0 +1,23 @@
validate.validators.equals = function(value, options, key, attributes) {
if (options && value !== options.value) {
return key + 'must equal ' + options.value;
}
};
validate.validators.doesNotEqual = function(value, options, key) {
if (options && value === options.value) {
return key + 'cannot equal ' + options.value;
}
};
validate.validators.contains = function(value, options, key) {
if (options && value.indexOf && value.indexOf(options.value) === -1) {
return key + 'must contain ' + options.value;
}
};
validate.validators.doesNotContain = function(value, options, key) {
if (options && value.indexOf && value.indexOf(options.value) !== -1) {
return key + 'cannot contain ' + options.value;
}
};
-656
View File
@@ -1,656 +0,0 @@
/**
* This is a temporary function which will return a hardcoded hanging protocol as a JavaScript object
*/
function getMammoHangingProtocolObject() {
var protocol = [{
stage: 1,
rows: 2,
columns: 4,
viewports: [{
seriesDescription: 'RCC',
study: 'prior'
}, {
seriesDescription: 'LCC',
study: 'prior'
}, {
seriesDescription: 'RMLO',
study: 'prior'
}, {
seriesDescription: 'LMLO',
study: 'prior'
}, {
seriesDescription: 'RCC',
study: 'current'
}, {
seriesDescription: 'LCC',
study: 'current'
}, {
seriesDescription: 'RMLO',
study: 'current'
}, {
seriesDescription: 'LMLO',
study: 'current'
}
]
}, {
stage: 2,
rows: 1,
columns: 2,
viewports: [{
seriesDescription: 'RCC',
study: 'current'
}, {
seriesDescription: 'LCC',
study: 'current'
}
]
}, {
stage: 3,
rows: 1,
columns: 2,
viewports: [{
seriesDescription: 'RMLO',
study: 'current'
}, {
seriesDescription: 'LMLO',
study: 'current'
}
]
}, {
stage: 4,
rows: 1,
columns: 2,
viewports: [{
seriesDescription: 'RCC',
study: 'current'
}, {
seriesDescription: 'RCC',
study: 'prior'
}
]
}, {
stage: 5,
rows: 1,
columns: 2,
viewports: [{
seriesDescription: 'LCC',
study: 'current'
}, {
seriesDescription: 'LCC',
study: 'prior'
}
]
}, {
stage: 6,
rows: 1,
columns: 2,
viewports: [{
seriesDescription: 'LMLO',
study: 'current'
}, {
seriesDescription: 'LMLO',
study: 'prior'
}
]
}, {
stage: 7,
rows: 1,
columns: 2,
viewports: [{
seriesDescription: 'RMLO',
study: 'current'
}, {
seriesDescription: 'RMLO',
study: 'prior'
}
]
}, {
stage: 8,
rows: 2,
columns: 4,
viewports: [{
seriesDescription: 'RCC',
study: 'prior',
options: {
includeCADMarkers: true
}
}, {
seriesDescription: 'LCC',
study: 'prior',
options: {
includeCADMarkers: true
}
}, {
seriesDescription: 'RMLO',
study: 'prior',
options: {
includeCADMarkers: true
}
}, {
seriesDescription: 'LMLO',
study: 'prior',
options: {
includeCADMarkers: true
}
}, {
seriesDescription: 'RCC',
study: 'current',
options: {
includeCADMarkers: true
}
}, {
seriesDescription: 'LCC',
study: 'current',
options: {
includeCADMarkers: true
}
}, {
seriesDescription: 'RMLO',
study: 'current',
options: {
includeCADMarkers: true
}
}, {
seriesDescription: 'LMLO',
study: 'current',
options: {
includeCADMarkers: true
}
}
]
}
];
return protocol;
}
function findSeriesByDescription(seriesDescription, study) {
var seriesInstanceUid;
study.seriesList.forEach(function(series) {
if (!series.seriesDescription) {
return;
}
var currentSeriesDescription = series.seriesDescription.replace(' ', '');
if (currentSeriesDescription === seriesDescription) {
seriesInstanceUid = series.seriesInstanceUid;
return false;
}
});
return seriesInstanceUid;
}
/**
* (Work in progress) Uses the information from a DICOM Hanging Protocol
* to identify and display studies and series in the image viewer
*
* @param hangingProtocol
* @param inputData
* @returns {Array} Array of viewport data to be displayed
*/
function applyHangingProtocol(hangingProtocol, inputData) {
var presentationGroup = inputData.DisplaySetPresentationGroup || 1;
var studies = ViewerStudies;
var currentProtocolData = hangingProtocol[presentationGroup - 1];
var viewportData = {
viewports: [],
viewportRows: currentProtocolData.rows,
viewportColumns: currentProtocolData.columns
};
var currentStudy = ViewerStudies.find({}, {$sort: {studyDate: 1}}).fetch()[0];
var otherStudies = WorklistStudies.find({
patientId: currentStudy.patientId,
studyInstanceUid: {
$ne: currentStudy.studyInstanceUid
}
}, {$sort: {
studyDate: 1
}}).fetch();
var priorStudy = otherStudies[0];
currentProtocolData.viewports.forEach(function(viewport, index) {
if (viewport.study === 'current') {
study = currentStudy;
} else if (viewport.study === 'prior') {
study = currentStudy;
/*ViewerStudies.find({
studyInstanceUid: {
$ne: currentStudy.studyInstanceUid
}
}, {$sort: {studyDate: 1}}).fetch()[0];*/
}
seriesInstanceUid = findSeriesByDescription(viewport.seriesDescription, study);
viewportData.viewports[index] = {
seriesInstanceUid: seriesInstanceUid,
studyInstanceUid: study.studyInstanceUid,
currentImageIdIndex: 0,
options: viewport.options
};
});
return viewportData;
}
/**
* This is a temporary function which will return a hardcoded hanging protocol as a JavaScript object
* The purpose of this is to act as a stub until we are actually parsing DICOM Hanging Protocol files,
* since Orthanc doesn't seem to support them yet.
*
* Soon we will be using instanceDataToJsObject to produce this object from the HangingProtocol WADO
* instance
*
*/
function getMammoHangingProtocol() {
var tagValues = {
HangingProtocolStorage: '1.2.840.10008.5.1.4.38.1'
};
var protocol = {
sopClassUid: tagValues.HangingProtocolStorage,
sopInstanceUid: '1.2.840.113986.2.664566.21121125.85669.911', // A random Uid
hangingProtocolName: 'MammoCadProtocol',
hangingProtocolDescription: 'Mammography screening protocol',
hangingProtocolLevel: 'SITE',
hangingProtocolCreator: 'Erik',
hangingProtocolCreationDateTime: '20020101104200',
hangingProtocolDefinitionSequence: [{
modality: 'MG',
laterality: '',
procedureCodeSequence: {
codeValue: 98765,
codingSchemeDesignator: '99Local',
codingSchemeVersion: 1.5,
codeMeaning: 'Mammogram'
},
anatomicRegionSequence: {
codeValue: 'T-D1100',
codingSchemeDesignator: 'SNM3',
codeMeaning: 'BREAST'
},
reasonForRequestedProcedureCodeSequence: {
codeValue: 'I67.1',
codingSchemeDesignator: 'I10',
codeMeaning: 'Calcification'
}
}],
hangingProtocolUserIdentificationCodeSequence: [],
hangingProtocolUserGroupName: 'ABC Hospital',
numberOfPriorsReferenced: 1,
imageSetsSequence: [{
imageSetSelectorSequence: [{
ImageSetSelectorUsageFlag: 'NO_MATCH',
SelectorAttribute: '00180015',
SelectorValueNumber: '1',
SelectorAttributeVR: 'CS',
SelectorCSValue: 'BREAST'
}],
timeBasedImageSetsSequence: [{
ImageSetNumber: 1,
ImageSetSelectorCategory: 'RELATIVE_TIME',
RelativeTime: '0\0',
RelativeTimeUnits: 'MINUTES',
ImageSetLabel: 'Current MG Breast'
},
{
ImageSetNumber: 2,
ImageSetSelectorCategory: 'ABSTRACT_PRIOR',
AbstractPriorValue: '1\\1',
ImageSetLabel: 'Prior MG Breast'
}]
}],
// Skip Number of Screens for now,
// Skip Nominal Screen Definition Sequence for now,
// http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.23.3.html
DisplaySetsSequence: [
{
// Left side image (R MLO Current)
ImageSetNumber: 1,
DisplaySetNumber: 1,
DisplaySetPresentationGroup: 1,
DisplaySetPresentationGroupDescription: 'Current Mediolateral only',
ImageBoxesSequence: [{
DisplayEnvironmentSpatialPosition: '0\0.2\0.16667\0',
ImageBoxNumber: 1,
ImageBoxLayoutType: 'STACK'
}],
FilterOperationsSequence: [{
SelectorAttributeVR: 'CS',
SelectorCSValue: 'R CC',
FilterByCategory: 'SERIES_DESCRIPTION',
FilterByOperator: 'MEMBER_OF'
}]
}, {
// Right side image (L MLO Current)
ImageSetNumber: 1,
DisplaySetNumber: 1,
DisplaySetPresentationGroup: 1,
ImageBoxesSequence: [{
ImageBoxNumber: 1,
ImageBoxLayoutType: 'STACK'
}],
FilterOperationsSequence: [{
SelectorAttributeVR: 'CS',
SelectorCSValue: 'L CC',
FilterByCategory: 'SERIES_DESCRIPTION',
FilterByOperator: 'MEMBER_OF'
}]
},
{
// Left side image (R MLO Current)
ImageSetNumber: 1,
DisplaySetNumber: 1,
DisplaySetPresentationGroup: 2,
DisplaySetPresentationGroupDescription: 'Current Mediolateral only',
ImageBoxesSequence: [{
DisplayEnvironmentSpatialPosition: '0\0.2\0.16667\0',
ImageBoxNumber: 1,
ImageBoxLayoutType: 'STACK'
}],
FilterOperationsSequence: [{
SelectorAttributeVR: 'CS',
SelectorCSValue: 'R MLO',
FilterByCategory: 'SERIES_DESCRIPTION',
FilterByOperator: 'MEMBER_OF'
}],
ReformattingOperationType: '',
ReformattingThickness: '',
ReformattingInterval: '',
ReformattingOperationInitialViewDirection: '',
SortingOperationsSequence: [],
DisplaySetPatientOrientation: '',
VOIType: ''
}, {
// Right side image (L MLO Current)
ImageSetNumber: 1,
DisplaySetNumber: 1,
DisplaySetPresentationGroup: 2,
ImageBoxesSequence: [{
ImageBoxNumber: 1,
ImageBoxLayoutType: 'STACK'
}],
FilterOperationsSequence: [{
SelectorAttributeVR: 'CS',
SelectorCSValue: 'L MLO',
FilterByCategory: 'SERIES_DESCRIPTION',
FilterByOperator: 'MEMBER_OF'
}]
}, {
// Testing purposes
// Left side image (R MLO Current)
ImageSetNumber: 1,
DisplaySetNumber: 1,
DisplaySetPresentationGroup: 3,
DisplaySetPresentationGroupDescription: 'Current Mediolateral only',
ImageBoxesSequence: [{
DisplayEnvironmentSpatialPosition: '0\0.2\0.16667\0',
ImageBoxNumber: 1,
ImageBoxLayoutType: 'STACK'
}],
FilterOperationsSequence: [{
SelectorAttributeVR: 'CS',
SelectorCSValue: 'R MLO',
FilterByCategory: 'SERIES_DESCRIPTION',
FilterByOperator: 'MEMBER_OF'
}]
}, {
// Right side image (R CC Current)
ImageSetNumber: 1,
DisplaySetNumber: 1,
DisplaySetPresentationGroup: 3,
ImageBoxesSequence: [{
ImageBoxNumber: 1,
ImageBoxLayoutType: 'STACK'
}],
FilterOperationsSequence: [{
SelectorAttributeVR: 'CS',
SelectorCSValue: 'R CC',
FilterByCategory: 'SERIES_DESCRIPTION',
FilterByOperator: 'MEMBER_OF'
}]
}],
PartialDataDisplayHandling: 'MAINTAIN_LAYOUT',
SynchronizedScrollingSequence: [],
NavigationIndicatorSequence: []
};
return protocol;
}
function findSetsByPresentationGroup(DisplaySetsSequence, DisplaySetPresentationGroup) {
var presentationGroupSets = [];
DisplaySetsSequence.forEach(function(displaySet) {
if (displaySet.DisplaySetPresentationGroup === DisplaySetPresentationGroup) {
presentationGroupSets.push(displaySet);
}
});
return presentationGroupSets;
}
function findStudy(displaySet, studies) {
// Placeholder for now
return studies.find().fetch()[0].studyInstanceUid;
}
/**
* Uses the FilterOperationsSequence information to find
* the relevant series for display in this display set
*
* @param displaySet
* @param study
* @returns {String} The instance Uid of the series to be displayed
*/
function findSeries(displaySet, study) {
var categoryDictionary = {
SERIES_DESCRIPTION: 'seriesDescription'
};
// TODO=Put these inside a loop? We need to support multiple filter operations,
// not just one
var filterBy = displaySet.FilterOperationsSequence[0].FilterByCategory;
var selectorValue = displaySet.FilterOperationsSequence[0].SelectorCSValue;
var filterType = displaySet.FilterOperationsSequence[0].FilterByOperator;
var category = categoryDictionary[filterBy];
var seriesInstanceUid;
study.seriesList.forEach(function(series) {
// TODO = Add other FilterBy operators
if (filterType === 'MEMBER_OF') {
if (series[category] === selectorValue) {
seriesInstanceUid = series.seriesInstanceUid;
return false;
}
}
});
return seriesInstanceUid;
}
/**
* (Work in progress) Uses the information from a DICOM Hanging Protocol
* to identify and display studies and series in the image viewer
*
* @param hangingProtocol
* @param inputData
* @returns {Array} Array of viewport data to be displayed
*/
function applyDICOMHangingProtocol(hangingProtocol, inputData) {
log.info('applyDICOMHangingProtocol');
var presentationGroup = inputData.DisplaySetPresentationGroup || 1;
var studies = ViewerStudies;
var currentDisplaySets = findSetsByPresentationGroup(hangingProtocol.DisplaySetsSequence, presentationGroup);
var viewportData = {
viewports: []
};
currentDisplaySets.forEach(function(displaySet, index) {
// TODO= Find study information by image set properties and
// image set number of this display set
var ImageSetNumber = displaySet.ImageSetNumber;
studyInstanceUid = findStudy(displaySet, studies);
var study = ViewerStudies.findOne({studyInstanceUid: studyInstanceUid});
seriesInstanceUid = findSeries(displaySet, study);
viewportData.viewports[index] = {
seriesInstanceUid: seriesInstanceUid,
studyInstanceUid: studyInstanceUid,
currentImageIdIndex: 0
};
});
return viewportData;
}
/**
* This is an example of a hanging protocol
* It takes in a set of studies, as well as the
* number of rows and columns in the layout.
*
* It returns an array of objects, one for each viewport, detailing
* which series should be loaded in the viewport.
*
* @param inputData
* @returns {Array}
*/
function defaultHangingProtocol(inputData) {
var studies = ViewerStudies.find().fetch();
var viewportRows = inputData.viewportRows;
var viewportColumns = inputData.viewportColumns;
// This is the most basic hanging protocol.
var stacks = [];
studies.forEach(function(study) {
study.seriesList.forEach(function(series) {
// Ensure that the series has image data
// (All images have rows)
var anInstance = series.instances[0];
if (!anInstance || !anInstance.rows) {
return;
}
var stack = {
series: series,
study: study
};
stacks.push(stack);
});
});
var viewportData = {
viewports: []
};
var numViewports = viewportRows * viewportColumns;
for (var i=0; i < numViewports; ++i) {
if (i >= stacks.length) {
// We don't have enough stacks to fill the desired number of viewports, so stop here
break;
}
viewportData.viewports[i] = {
seriesInstanceUid: stacks[i].series.seriesInstanceUid,
studyInstanceUid: stacks[i].study.studyInstanceUid,
currentImageIdIndex: 0
};
}
return viewportData;
}
var hangingProtocol;
var presentationGroup = 1;
function getHangingProtocol(inputData) {
// TODO = Update this to use Collection logic
var studies = ViewerStudies.find().fetch();
if (!studies.length) {
log.warn("No studies provided to Hanging Protocol");
return;
}
// Find the unique modalities in this study
var modalities = [];
studies.forEach(function(study) {
study.seriesList.forEach(function(series) {
if (modalities.indexOf(series.modality) < 0) {
modalities.push(series.modality);
}
});
});
if (modalities.indexOf('MG') > -1) {
var hp = getMammoHangingProtocolObject();
Session.set('WindowManagerPresentationGroup', presentationGroup);
var testData = applyHangingProtocol(hp, inputData);
return testData;
// Commenting this out for now so we can hardcode a MG protocol
/*var hangingProtocol = getMammoHangingProtocol();
Session.set('WindowManagerPresentationGroup', presentationGroup);
return applyDICOMHangingProtocol(hangingProtocol, inputData);*/
}
Session.set('WindowManagerPresentationGroup', undefined);
return defaultHangingProtocol(inputData);
}
function setHangingProtocol(protocol) {
hangingProtocol = protocol;
}
function previousPresentationGroup() {
presentationGroup--;
presentationGroup = Math.max(1, presentationGroup);
log.info('previousPresentationGroup: ' + presentationGroup);
Session.set('WindowManagerPresentationGroup', presentationGroup);
Session.set('UseHangingProtocol', Random.id());
}
function nextPresentationGroup() {
var numPresentationGroups = getNumPresentationGroups();
presentationGroup++;
presentationGroup = Math.min(numPresentationGroups, presentationGroup);
log.info('nextPresentationGroup: ' + presentationGroup);
Session.set('WindowManagerPresentationGroup', presentationGroup);
Session.set('UseHangingProtocol', Random.id());
}
function getNumPresentationGroups() {
// TODO=Pull this information from the largest
// value of DisplaySetPresentationGroup in the DisplaySetsSequence
// after the DICOM-HP is parsed
return 8;
}
function getCurrentPresentationGroup() {
return presentationGroup;
}
function setCurrentPresentationGroup(groupNumber) {
// Unused for now
presentationGroup = groupNumber;
}
WindowManager = {
setHangingProtocol: setHangingProtocol,
getHangingProtocol: getHangingProtocol,
getNumPresentationGroups: getNumPresentationGroups,
setCurrentPresentationGroup: setCurrentPresentationGroup,
getCurrentPresentationGroup: getCurrentPresentationGroup,
nextPresentationGroup: nextPresentationGroup,
previousPresentationGroup: previousPresentationGroup
};
WindowManager.setHangingProtocol(defaultHangingProtocol);
+38 -68
View File
@@ -5,64 +5,56 @@ Package.describe({
});
Package.onUse(function(api) {
api.versionsFrom('1.2.0.2');
api.versionsFrom('1.2.1');
api.use('standard-app-packages');
api.use('ecmascript');
api.use('http');
api.use('jquery');
api.use('stylus');
api.use('accounts-password');
api.use('ian:accounts-ui-bootstrap-3');
api.use('http');
api.use('practicalmeteor:loglevel');
api.use('momentjs:moment');
// Our custom packages
api.use('dicomweb');
api.use('dimseservice');
api.use('cornerstone');
api.use('hangingprotocols');
api.addFiles('log.js', ['client', 'server']);
api.addFiles('log.js');
// TODO= Find a meteor package for this
// TODO: Use NPM depends for these
api.addFiles('client/compatibility/jquery.hotkeys.js', 'client');
// Data validation (the Meteor package is currently out-of-date)
api.addFiles('client/compatibility/validate.js', 'client');
// ---------- Collections ----------
api.addFiles('client/collections.js', 'client');
// ---------- Components ----------
// Basic components
api.addFiles('client/components/basic/layout/layout.html', 'client');
api.addFiles('client/components/basic/layout/layout.styl', 'client');
api.addFiles('client/components/basic/login/login.html', 'client');
api.addFiles('client/components/basic/notFound/notFound.html', 'client');
api.addFiles('client/components/basic/loadingText/loadingText.html', 'client');
api.addFiles('client/components/basic/loadingText/loadingText.styl', 'client');
api.addFiles('client/components/basic/studyNotFound/studyNotFound.html', 'client');
api.addFiles('client/components/basic/studyNotFound/studyNotFound.styl', 'client');
api.addFiles('client/components/basic/removableBackdrop/removableBackdrop.html', 'client');
api.addFiles('client/components/basic/removableBackdrop/removableBackdrop.styl', 'client');
api.addFiles('client/components/basic/hidingPanel/hidingPanel.html', 'client');
api.addFiles('client/components/basic/hidingPanel/hidingPanel.js', 'client');
api.addFiles('client/components/basic/hidingPanel/hidingPanel.styl', 'client');
api.addFiles('client/components/basic/confirmDeleteDialog/confirmDeleteDialog.html', 'client');
api.addFiles('client/components/basic/confirmDeleteDialog/confirmDeleteDialog.styl', 'client');
api.addFiles('client/components/basic/confirmDeleteDialog/confirmDeleteDialog.js', 'client');
// Study Browser components
api.addFiles('client/components/studyBrowser/studyBrowser/studyBrowser.html', 'client');
api.addFiles('client/components/studyBrowser/studyBrowser/studyBrowser.js', 'client');
api.addFiles('client/components/studyBrowser/studyBrowser/studyBrowser.styl', 'client');
api.addFiles('client/components/studyBrowser/relatedStudySelect/relatedStudySelect.html', 'client');
api.addFiles('client/components/studyBrowser/relatedStudySelect/relatedStudySelect.js', 'client');
api.addFiles('client/components/studyBrowser/relatedStudySelect/relatedStudySelect.styl', 'client');
api.addFiles('client/components/studyBrowser/thumbnailEntry/thumbnailEntry.html', 'client');
api.addFiles('client/components/studyBrowser/thumbnailEntry/thumbnailEntry.js', 'client');
api.addFiles('client/components/studyBrowser/thumbnailEntry/thumbnailEntry.styl', 'client');
api.addFiles('client/components/studyBrowser/thumbnails/thumbnails.html', 'client');
api.addFiles('client/components/studyBrowser/thumbnails/thumbnails.js', 'client');
api.addFiles('client/components/studyBrowser/imageThumbnail/imageThumbnail.html', 'client');
api.addFiles('client/components/studyBrowser/imageThumbnail/imageThumbnail.js', 'client');
api.addFiles('client/components/studyBrowser/imageThumbnail/imageThumbnail.styl', 'client');
@@ -72,9 +64,9 @@ Package.onUse(function(api) {
api.addFiles('client/components/viewer/imageViewerViewport/imageViewerViewport.js', 'client');
api.addFiles('client/components/viewer/imageViewerViewport/imageViewerViewport.styl', 'client');
api.addFiles('client/components/viewer/imageViewerViewports/imageViewerViewports.html', 'client');
api.addFiles('client/components/viewer/imageViewerViewports/imageViewerViewports.js', 'client');
api.addFiles('client/components/viewer/imageViewerViewports/imageViewerViewports.styl', 'client');
api.addFiles('client/components/viewer/gridLayout/gridLayout.html', 'client');
api.addFiles('client/components/viewer/gridLayout/gridLayout.js', 'client');
api.addFiles('client/components/viewer/gridLayout/gridLayout.styl', 'client');
api.addFiles('client/components/viewer/loadingIndicator/loadingIndicator.html', 'client');
api.addFiles('client/components/viewer/loadingIndicator/loadingIndicator.js', 'client');
@@ -105,34 +97,22 @@ Package.onUse(function(api) {
api.addFiles('client/components/viewer/playClipButton/playClipButton.html', 'client');
api.addFiles('client/components/viewer/playClipButton/playClipButton.js', 'client');
api.addFiles('client/components/viewer/hangingProtocolButtons/hangingProtocolButtons.html', 'client');
api.addFiles('client/components/viewer/hangingProtocolButtons/hangingProtocolButtons.js', 'client');
api.addFiles('client/components/viewer/layoutButton/layoutButton.html', 'client');
api.addFiles('client/components/viewer/toolbar/toolbar.html', 'client');
api.addFiles('client/components/viewer/toolbar/toolbar.js', 'client');
api.addFiles('client/components/viewer/toolbar/toolbar.styl', 'client');
api.addFiles('client/components/basic/progressDialog/progressDialog.html', 'client');
api.addFiles('client/components/basic/progressDialog/progressDialog.styl', 'client');
api.addFiles('client/components/basic/progressDialog/progressDialog.js', 'client');
api.addFiles('client/components/viewer/toolbarGroupButton/toolbarGroupButton.html', 'client');
api.addFiles('client/components/viewer/toolbarGroupButton/toolbarGroupButton.styl', 'client');
api.addFiles('client/components/viewer/toolbarGroupButton/toolbarGroupButton.js', 'client');
// Library functions
api.addFiles('lib/accountsConfig.js', 'client');
api.addFiles('lib/layoutManager.js', 'client');
api.addFiles('lib/createStacks.js', 'client');
api.addFiles('lib/getImageId.js', 'client');
api.addFiles('lib/getWADORSImageId.js', 'client');
api.addFiles('lib/metaDataProvider.js', 'client');
api.addFiles('lib/rerenderViewportWithNewSeries.js', 'client');
api.addFiles('lib/sortStudy.js', 'client');
api.addFiles('lib/toolManager.js', 'client');
api.addFiles('lib/windowManager.js', 'client');
api.addFiles('lib/enablePrefetchOnElement.js', 'client');
api.addFiles('lib/displayReferenceLines.js', 'client');
api.addFiles('lib/draggable.js', 'client');
@@ -147,11 +127,11 @@ Package.onUse(function(api) {
api.addFiles('lib/setFocusToActiveViewport.js', 'client');
api.addFiles('lib/updateAllViewports.js', 'client');
api.addFiles('lib/queryStudies.js', 'client');
api.addFiles('lib/exportStudies.js', 'client');
api.addFiles('lib/importStudies.js', 'client');
api.addFiles('lib/encodeQueryData.js', 'server');
api.addFiles('lib/validators.js', 'client');
api.addFiles('lib/instanceClassSpecificViewport.js', 'client');
api.addFiles('lib/setMammogramViewportAlignment.js', 'client');
//api.export('accountsConfig', 'client');
api.export('resizeViewportElements', 'client');
api.export('handleResize', 'client');
api.export('enableHotkeys', 'client');
@@ -162,7 +142,6 @@ Package.onUse(function(api) {
api.export('getImageId', 'client');
api.export('getWADORSImageId', 'client');
api.export('metaDataProvider', 'client');
api.export('rerenderViewportWithNewSeries', 'client');
api.export('sortStudy', 'client');
api.export('updateOrientationMarkers', 'client');
api.export('setFocusToActiveViewport', 'client');
@@ -172,55 +151,46 @@ Package.onUse(function(api) {
api.export('exportStudies', 'client');
api.export('importStudies', 'client');
api.export('getActiveViewportElement', 'client');
api.export('encodeQueryData', 'server');
api.export('getInstanceClassDefaultViewport', 'client');
api.export('showConfirmDialog', 'client');
api.export('applyWLPreset', 'client');
// Export the ValidateJS Library with our added validators
//api.export('validate', 'client');
// Viewer management objects
api.export('toolManager', 'client');
api.export('WindowManager', 'client');
api.export('LayoutManager', 'client');
// Global objects
api.export('OHIF', 'client');
api.export('ClientId', 'client');
api.export('progressDialog', 'client');
// Collections
api.export('ViewerStudies', 'client');
// UI Helpers
api.addFiles('lib/helpers/formatDA.js', 'client');
api.addFiles('lib/helpers/formatJSDate.js', 'client');
api.addFiles('lib/helpers/jsDateFromNow.js', 'client');
api.addFiles('lib/helpers/formatNumberPrecision.js', 'client');
api.addFiles('lib/helpers/formatTM.js', 'client');
api.addFiles('lib/helpers/inlineIf.js', 'client');
api.addFiles('lib/helpers/inc.js', 'client');
api.addFiles('lib/helpers/getUsername.js', 'client');
api.addFiles('lib/helpers/capitalizeFirstLetter.js', 'client');
api.addFiles('lib/helpers/objectToPairs.js', 'client');
api.addFiles('lib/helpers/objectEach.js', 'client');
api.addFiles('lib/helpers/ifTypeIs.js', 'client');
api.addFiles('lib/helpers/prettyPrintStringify.js', 'client');
api.addFiles('lib/helpers/formatPN.js', 'client');
api.export('formatPN', 'client');
api.addFiles('lib/helpers/isTouchDevice.js', 'client');
api.export('isTouchDevice', 'client');
// Server-side functions
api.addFiles('server/seed.js', 'server');
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');
api.addFiles('server/services/qido/studies.js', 'server');
api.addFiles('server/services/wado/retrieveMetadata.js', 'server');
// DIMSE instance, study, and metadata retrieval
api.addFiles('server/services/dimse/instances.js', 'server');
api.addFiles('server/services/dimse/studies.js', 'server');
api.addFiles('server/services/dimse/retrieveMetadata.js', 'server');
// Study, instance, and metadata retrieval from remote PACS via Orthanc as a proxy
api.addFiles('server/services/remote/instances.js', 'server');
api.addFiles('server/services/remote/studies.js', 'server');
api.addFiles('server/services/remote/retrieveMetadata.js', 'server');
api.export('Services', 'server');
api.export('importStudies', 'server');
api.export('importSupported', 'server');
@@ -1,9 +0,0 @@
Services = {};
Services.QIDO = {};
Services.WADO = {};
Services.DIMSE = {};
Services.REMOTE = {};
remoteGetValue = function(obj) {
return obj ? obj.Value : null;
}
@@ -1,22 +0,0 @@
Meteor.methods({
/**
* Retrieves Study metadata given a Study Instance UID
* This Meteor method is available from both the client and the server
*/
GetStudyMetadata: function(studyInstanceUid) {
log.info('GetStudyMetadata(%s)', studyInstanceUid);
if (!Meteor.settings.dicomWeb) {
throw 'No properly configured server was available over DICOMWeb';
}
if (Meteor.settings.dicomWeb && Meteor.settings.defaultServiceType === 'dicomWeb') {
// Get the server data. This is user-defined in the
// config.json files used to run the Meteor server
var server = Meteor.settings.dicomWeb.endpoints[0];
return Services.WADO.RetrieveMetadata(server, studyInstanceUid);
} else if (Meteor.settings.dimse && Meteor.settings.defaultServiceType === 'dimse') {
return Services.DIMSE.RetrieveMetadata(studyInstanceUid);
}
}
});
@@ -1,19 +0,0 @@
Meteor.methods({
/**
* Use the specified filter to conduct a search from the DICOM server
* @param filter
*/
WorklistSearch: function(filter) {
if (Meteor.settings.dicomWeb && Meteor.settings.defaultServiceType === 'dicomWeb') {
// Get the server data. This is user-defined in the
// config.json files used to run the Meteor server
var server = Meteor.settings.dicomWeb.endpoints[0];
return Services.QIDO.Studies(server, filter);
} else if (Meteor.settings.dimse && Meteor.settings.defaultServiceType === 'dimse') {
return Services.DIMSE.Studies(filter);
} else {
throw 'No properly configured server was available over DICOMWeb or DIMSE.';
}
}
});
-18
View File
@@ -1,18 +0,0 @@
var users = [
{
email: 'admin@ohif.org',
//username: "admin",
password: "admin",
profile: {
}
}
];
Meteor.startup(function() {
users.forEach(function(user) {
if(!Meteor.users.findOne({username: user.username})) {
Accounts.createUser(user);
}
});
});
@@ -1,66 +0,0 @@
/**
* Parses data returned from a study search and transforms it into
* an array of series that are present in the study
*
* @param resultData
* @returns {Array} Series List
*/
function resultDataToStudyMetadata(resultData) {
var seriesMap = {};
var seriesList = [];
resultData.forEach(function(instanceRaw) {
var instance = instanceRaw.toObject();
// Use seriesMap to cache series data
// If the series instance UID has already been used to
// process series data, continue using that series
var seriesInstanceUid = instance[0x0020000E];
var series = seriesMap[seriesInstanceUid];
// If no series data exists in the seriesMap cache variable,
// process any available series data
if (!series) {
series = {
seriesInstanceUid: seriesInstanceUid,
seriesNumber: instance[0x00200011],
instances: []
};
// Save this data in the seriesMap cache variable
seriesMap[seriesInstanceUid] = series;
seriesList.push(series);
}
var host = Meteor.settings.dimse.host,
port = Meteor.settings.dimse.port;
var serverRoot = host + ':' + port;
var sopInstanceUid = instance[0x00080018];
var uri = serverRoot + '/studies/' + studyInstanceUid + '/series/' + seriesInstanceUid + '/instances/' + sopInstanceUid + '/frames/1';
// Add this instance to the current series
series.instances.push({
sopClassUid: instance[0x00080016],
sopInstanceUid: sopInstanceUid,
uri: uri,
instanceNumber: instance[0x00200013]
});
});
return seriesList;
}
/**
* Retrieve a set of instances using a DIMSE call
* @param studyInstanceUid
* @returns {{wadoUriRoot: String, studyInstanceUid: String, seriesList: Array}}
*/
Services.DIMSE.Instances = function(studyInstanceUid) {
//var url = buildUrl(server, studyInstanceUid);
var result = DIMSE.retrieveInstances(studyInstanceUid);
return {
studyInstanceUid: studyInstanceUid,
seriesList: resultDataToStudyMetadata(result)
};
};
@@ -1,126 +0,0 @@
/**
* Parses the SourceImageSequence, if it exists, in order
* to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID
* is used to refer to this image in any accompanying DICOM-SR documents.
*
* @param instance
* @returns {String} The ReferenceSOPInstanceUID
*/
function getSourceImageInstanceUid(instance) {
// TODO= Parse the whole Source Image Sequence
// This is a really poor workaround for now.
// Later we should probably parse the whole sequence.
var SourceImageSequence = instance[0x00082112];
log.info('SourceImageSequence ' + SourceImageSequence);
if (SourceImageSequence && SourceImageSequence.length) {
return SourceImageSequence[0][0x00081155];
}
}
/**
* Parses result data from a DIMSE search into Study MetaData
* Returns an object populated with study metadata, including the
* series list.
*
* @param resultData
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
*/
function resultDataToStudyMetadata(studyInstanceUid, resultData) {
log.info('resultDataToStudyMetadata');
var seriesMap = {};
var seriesList = [];
if (!resultData.length) {
return;
}
var anInstance = resultData[0].toObject();
if (!anInstance) {
return;
}
var studyData = {
seriesList: seriesList,
patientName: anInstance[0x00100010],
patientId: anInstance[0x00100020],
accessionNumber: anInstance[0x00080050],
studyDate: anInstance[0x00080020],
modalities: anInstance[0x00080061],
studyDescription: anInstance[0x00081030],
imageCount: anInstance[0x00201208],
studyInstanceUid: anInstance[0x0020000D]
};
resultData.forEach(function(instanceRaw) {
var instance = instanceRaw.toObject();
var seriesInstanceUid = instance[0x0020000E];
var series = seriesMap[seriesInstanceUid];
if (!series) {
series = {
seriesDescription: instance[0x0008103E],
modality: instance[0x00080060],
seriesInstanceUid: seriesInstanceUid,
seriesNumber: parseFloat(instance[0x00200011]),
instances: []
};
seriesMap[seriesInstanceUid] = series;
seriesList.push(series);
}
var sopInstanceUid = instance[0x00080018];
var instanceSummary = {
imageType: instance[0x00080008],
sopClassUid: instance[0x00080016],
sopInstanceUid: sopInstanceUid,
instanceNumber: parseFloat(instance[0x00200013]),
imagePositionPatient: instance[0x00200032],
imageOrientationPatient: instance[0x00200037],
frameOfReferenceUID: instance[0x00200052],
sliceLocation: parseFloat(instance[0x00201041]),
samplesPerPixel: parseFloat(instance[0x00280002]),
photometricInterpretation: instance[0x00280004],
rows: parseFloat(instance[0x00280010]),
columns: parseFloat(instance[0x00280011]),
pixelSpacing: instance[0x00280030],
bitsAllocated: parseFloat(instance[0x00280100]),
bitsStored: parseFloat(instance[0x00280101]),
highBit: parseFloat(instance[0x00280102]),
pixelRepresentation: parseFloat(instance[0x00280103]),
windowCenter: instance[0x00281050],
windowWidth: instance[0x00281051],
rescaleIntercept: parseFloat(instance[0x00281052]),
rescaleSlope: parseFloat(instance[0x00281053]),
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
laterality: instance[0x00200062],
viewPosition: instance[0x00185101],
numFrames: parseFloat(instance[0x00280008]),
frameTime: parseFloat(instance[0x00181063])
};
// Retrieve the actual data over WADO-URI
var server = Meteor.settings.dicomWeb.endpoints[0];
instanceSummary.wadouri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom";
series.instances.push(instanceSummary);
});
return studyData;
}
/**
* Retrieved Study MetaData from a DICOM server using DIMSE
* @param studyInstanceUid
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
*/
Services.DIMSE.RetrieveMetadata = function(studyInstanceUid) {
var result = DIMSE.retrieveInstances(studyInstanceUid);
var study = resultDataToStudyMetadata(studyInstanceUid, result);
if (!study) {
study = {};
}
study.studyInstanceUid = studyInstanceUid;
return study;
};
@@ -1,47 +0,0 @@
/**
* Parses resulting data from a QIDO call into a set of Study MetaData
*
* @param resultData
* @returns {Array} An array of Study MetaData objects
*/
function resultDataToStudies(resultData) {
var studies = [];
resultData.forEach(function(studyRaw) {
var study = studyRaw.toObject();
studies.push({
studyInstanceUid: study[0x0020000D],
// 00080005 = SpecificCharacterSet
studyDate: study[0x00080020],
studyTime: study[0x00080030],
accessionNumber: study[0x00080050],
referringPhysicianName: study[0x00080090],
// 00081190 = URL
patientName: study[0x00100010],
patientId: study[0x00100020],
patientBirthdate: study[0x00100030],
patientSex: study[0x00100040],
imageCount: study[0x00201208],
studyId: study[0x00200010],
studyDescription: study[0x00081030],
modalities: study[0x00080061]
});
});
return studies;
}
Services.DIMSE.Studies = function(filter) {
log.info('Services.DIMSE.Studies');
var parameters = {
0x00100010: filter.patientName,
0x00100020: filter.patientId,
0x00080050: filter.accessionNumber,
0x00081030: filter.studyDescription,
0x00100040: '',
0x00201208: '',
0x00080061: ''
};
var results = DIMSE.retrieveStudies(parameters);
return resultDataToStudies(results);
};
@@ -1,83 +0,0 @@
/**
* Creates a QIDO URL given the server settings and a study instance UID
* @param server
* @param studyInstanceUid
* @returns {string} URL to be used for QIDO calls
*/
function buildUrl(server, studyInstanceUid) {
return server.qidoRoot + '/studies/' + studyInstanceUid + '/instances';
}
/**
* Parses data returned from a QIDO search and transforms it into
* an array of series that are present in the study
*
* @param server The DICOM server
* @param studyInstanceUid
* @param resultData
* @returns {Array} Series List
*/
function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
var seriesMap = {};
var seriesList = [];
resultData.forEach(function(instance) {
// Use seriesMap to cache series data
// If the series instance UID has already been used to
// process series data, continue using that series
var seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
var series = seriesMap[seriesInstanceUid];
// If no series data exists in the seriesMap cache variable,
// process any available series data
if (!series) {
series = {
seriesInstanceUid: seriesInstanceUid,
seriesNumber: DICOMWeb.getString(instance['00200011']),
instances: []
};
// Save this data in the seriesMap cache variable
seriesMap[seriesInstanceUid] = series;
seriesList.push(series);
}
// The uri for the dicomweb
// NOTE: DCM4CHEE seems to return the data zipped
// NOTE: Orthanc returns the data with multi-part mime which cornerstoneWADOImageLoader doesn't
// know how to parse yet
//var uri = DICOMWeb.getString(instance['00081190']);
//uri = uri.replace('wado-rs', 'dicom-web');
// manually create a WADO-URI from the UIDs
// NOTE: Haven't been able to get Orthanc's WADO-URI to work yet - maybe its not configured?
var sopInstanceUid = DICOMWeb.getString(instance['00080018']);
var uri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + '&contentType=application%2Fdicom';
// Add this instance to the current series
series.instances.push({
sopClassUid: DICOMWeb.getString(instance['00080016']),
sopInstanceUid: sopInstanceUid,
uri: uri,
instanceNumber: DICOMWeb.getString(instance['00200013'])
});
});
return seriesList;
}
/**
* Retrieve a set of instances using a QIDO call
* @param server
* @param studyInstanceUid
* @returns {{wadoUriRoot: String, studyInstanceUid: String, seriesList: Array}}
*/
Services.QIDO.Instances = function(server, studyInstanceUid) {
var url = buildUrl(server, studyInstanceUid);
var result = DICOMWeb.getJSON(url, server.requestOptions);
return {
wadoUriRoot: server.wadoUriRoot,
studyInstanceUid: studyInstanceUid,
seriesList: resultDataToStudyMetadata(server, studyInstanceUid, result.data)
};
};
@@ -1,65 +0,0 @@
/**
* Produces a QIDO URL given server details and a set of specified search filter
* items
*
* @param server
* @param filter
* @returns {string} The URL with encoded filter query data
*/
function filterToQIDOURL(server, filter) {
var commaSeparatedFields = [
'00081030', // Study Description
'00080060' //Modality
// Add more fields here if you want them in the Study List
].join(',');
var parameters = {
PatientName: filter.patientName,
PatientID: filter.patientId,
AccessionNumber: filter.accessionNumber,
StudyDescription: filter.studyDescription,
limit: filter.limit || 20,
includefield: server.qidoSupportsIncludeField ? 'all' : commaSeparatedFields
};
return server.qidoRoot + '/studies?' + encodeQueryData(parameters);
}
/**
* Parses resulting data from a QIDO call into a set of Study MetaData
*
* @param resultData
* @returns {Array} An array of Study MetaData objects
*/
function resultDataToStudies(resultData) {
var studies = [];
resultData.forEach(function(study) {
studies.push({
studyInstanceUid: DICOMWeb.getString(study['0020000D']),
// 00080005 = SpecificCharacterSet
studyDate: DICOMWeb.getString(study['00080020']),
studyTime: DICOMWeb.getString(study['00080030']),
accessionNumber: DICOMWeb.getNumber(study['00080050']),
referringPhysicianName: DICOMWeb.getString(study['00080090']),
// 00081190 = URL
patientName: DICOMWeb.getName(study['00100010']),
patientId: DICOMWeb.getString(study['00100020']),
patientBirthdate: DICOMWeb.getString(study['00100030']),
patientSex: DICOMWeb.getString(study['00100040']),
studyId: DICOMWeb.getString(study['00200010']),
numberOfStudyRelatedSeries: DICOMWeb.getString(study['00201206']),
numberOfStudyRelatedInstances: DICOMWeb.getString(study['00201208']),
studyDescription: DICOMWeb.getString(study['00081030']),
// modality: DICOMWeb.getString(study['00080060']),
// modalitiesInStudy: DICOMWeb.getString(study['00080061']),
modalities: DICOMWeb.getString(DICOMWeb.getModalities(study['00080060'], study['00080061']))
});
});
return studies;
}
Services.QIDO.Studies = function(server, filter) {
var url = filterToQIDOURL(server, filter);
var result = DICOMWeb.getJSON(url, server.requestOptions);
return resultDataToStudies(result.data);
};
@@ -1,82 +0,0 @@
/**
* Parses data returned from a QIDO search and transforms it into
* an array of series that are present in the study
*
* @param server The DICOM server
* @param studyInstanceUid
* @param resultData
* @returns {Array} Series List
*/
function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
var seriesMap = {};
var seriesList = [];
resultData.forEach(function(instance) {
// Use seriesMap to cache series data
// If the series instance UID has already been used to
// process series data, continue using that series
var seriesInstanceUid = remoteGetValue(instance['0020,000e']);
var series = seriesMap[seriesInstanceUid];
// If no series data exists in the seriesMap cache variable,
// process any available series data
if(!series) {
series = {
seriesInstanceUid : seriesInstanceUid,
seriesNumber : parseFloat(remoteGetValue(instance['0020,0011'])),
instances: []
};
// Save this data in the seriesMap cache variable
seriesMap[seriesInstanceUid] = series;
seriesList.push(series);
}
// The uri for the dicomweb
// NOTE: DCM4CHEE seems to return the data zipped
// NOTE: Orthanc returns the data with multi-part mime which cornerstoneWADOImageLoader doesn't
// know how to parse yet
//var uri = DICOMWeb.getString(instance['00081190']);
//uri = uri.replace('wado-rs', 'dicom-web');
// manually create a WADO-URI from the UIDs
// NOTE: Haven't been able to get Orthanc's WADO-URI to work yet - maybe its not configured?
var sopInstanceUid = remoteGetValue(instance['0008,0018']);
var uri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom";
// Add this instance to the current series
series.instances.push({
sopClassUid: remoteGetValue(instance['0008,0016']),
sopInstanceUid: sopInstanceUid,
uri: uri,
instanceNumber: parseFloat(remoteGetValue(instance['0020,0013']))
});
});
return seriesList;
}
/**
* Retrieve a set of instances using a QIDO call
* @param server
* @param studyInstanceUid
* @returns {{wadoUriRoot: String, studyInstanceUid: String, seriesList: Array}}
*/
Services.REMOTE.Instances = function(server, studyInstanceUid) {
var parameters = {
PatientName: "",
PatientID: "",
AccessionNumber: "",
SeriesInstanceUID: "",
SeriesNumber : "",
SOPClassUID : "",
InstanceNumber : ""
};
var remote = new OrthancRemote(server.root, server.sourceAE);
return {
wadoUriRoot: server.wadoUriRoot,
studyInstanceUid: studyInstanceUid,
seriesList: resultDataToStudyMetadata(server, studyInstanceUid, remote.findInstances(server.modality, studyInstanceUid, null, parameters))
};
};
@@ -1,133 +0,0 @@
/**
* Parses the SourceImageSequence, if it exists, in order
* to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID
* is used to refer to this image in any accompanying DICOM-SR documents.
*
* @param instance
* @returns {String} The ReferenceSOPInstanceUID
*/
function getSourceImageInstanceUid(instance) {
// TODO= Parse the whole Source Image Sequence
// This is a really poor workaround for now.
// Later we should probably parse the whole sequence.
var SourceImageSequence = remoteGetValue(instance['0008,2112']);
if (SourceImageSequence && SourceImageSequence.Value && SourceImageSequence.Value.length) {
return SourceImageSequence.Value[0]['0008,1155'].Value[0];
}
}
/**
* Parses result data from a WADO search into Study MetaData
* Returns an object populated with study metadata, including the
* series list.
*
* @param server
* @param studyInstanceUid
* @param resultData
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
*/
function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
var seriesMap = {};
var seriesList = [];
if (!resultData.length) {
return;
}
var anInstance = resultData[0];
if (!anInstance) {
return;
}
var studyData = {
seriesList: seriesList,
patientName: remoteGetValue(anInstance['0010,0010']),
patientId: remoteGetValue(anInstance['0010,0020']),
accessionNumber: remoteGetValue(anInstance['0008,0050']),
studyDate: remoteGetValue(anInstance['0008,0020']),
modalities: remoteGetValue(anInstance['0008,0061']),
studyDescription: remoteGetValue(anInstance['0008,1030']),
imageCount: remoteGetValue(anInstance['0020,1208']),
studyInstanceUid: remoteGetValue(anInstance['0020,000d'])
};
resultData.forEach(function(instance) {
var seriesInstanceUid = remoteGetValue(instance['0020,000e']);
var series = seriesMap[seriesInstanceUid];
if (!series) {
series = {
seriesDescription: remoteGetValue(instance['0008,103e']),
modality: remoteGetValue(instance['0008,0060']),
seriesInstanceUid: seriesInstanceUid,
seriesNumber: parseFloat(remoteGetValue(instance['0020,0011'])),
instances: []
};
seriesMap[seriesInstanceUid] = series;
seriesList.push(series);
}
var sopInstanceUid = remoteGetValue(instance['0008,0018']);
var instanceSummary = {
imageType: remoteGetValue(instance['0008,0008']),
sopClassUid: remoteGetValue(instance['0008,0016']),
sopInstanceUid: sopInstanceUid,
instanceNumber: parseFloat(remoteGetValue(instance['0020,0013'])),
imagePositionPatient: remoteGetValue(instance['0020,0032']),
imageOrientationPatient: remoteGetValue(instance['0020,0037']),
frameOfReferenceUID: remoteGetValue(instance['0020,0052']),
sliceLocation: parseFloat(remoteGetValue(instance['0020,1041'])),
samplesPerPixel: parseFloat(remoteGetValue(instance['0028,0002'])),
photometricInterpretation: remoteGetValue(instance['0028,0004']),
rows: parseFloat(remoteGetValue(instance['0028,0010'])),
columns: parseFloat(remoteGetValue(instance['0028,0011'])),
pixelSpacing: remoteGetValue(instance['0028,0030']),
bitsAllocated: parseFloat(remoteGetValue(instance['0028,0100'])),
bitsStored: parseFloat(remoteGetValue(instance['0028,0101'])),
highBit: parseFloat(remoteGetValue(instance['0028,0102'])),
pixelRepresentation: parseFloat(remoteGetValue(instance['0028,0103'])),
windowCenter: remoteGetValue(instance['0028,1050']),
windowWidth: remoteGetValue(instance['0028,1051']),
rescaleIntercept: parseFloat(remoteGetValue(instance['0028,1052'])),
rescaleSlope: parseFloat(remoteGetValue(instance['0028,1053'])),
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
laterality: remoteGetValue(instance['0020,0062']),
viewPosition: remoteGetValue(instance['0018,5101']),
numFrames: parseFloat(remoteGetValue(instance['0028,0008'])),
frameTime: parseFloat(remoteGetValue(instance['0018,1063']))
};
var iid = instance['xxxx,0001'].Value;
if (server.imageRendering === 'wadouri') {
instanceSummary.wadouri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom";
} else if (server.imageRendering == 'orthanc') {
instanceSummary.wadouri = server.root + '/instances/' + iid + '/file';
} else {
instanceSummary.wadorsuri = server.wadoRoot + '/studies/' + studyInstanceUid + '/series/' + seriesInstanceUid + '/instances/' + sopInstanceUid + '/frames/1';
}
series.instances.push(instanceSummary);
});
console.log(studyData.seriesList[0].instances);
return studyData;
}
/**
* Retrieved Study MetaData from a DICOM server using a WADO call
* @param server
* @param studyInstanceUid
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
*/
Services.REMOTE.RetrieveMetadata = function(server, studyInstanceUid) {
var remote = new OrthancRemote(server.root, server.sourceAE);
var study = resultDataToStudyMetadata(server, studyInstanceUid, remote.retrieveMetadata(server.modality, studyInstanceUid));
if (!study) {
study = {};
}
study.wadoUriRoot = server.wadoUriRoot;
study.studyInstanceUid = studyInstanceUid;
return study;
};
@@ -1,47 +0,0 @@
function resultDataToStudies(resultData) {
var studies = [];
resultData.forEach(function(study) {
studies.push({
studyInstanceUid: remoteGetValue(study['0020,000d']),
// 00080005 = SpecificCharacterSet
studyDate: remoteGetValue(study['0008,0020']),
studyTime: remoteGetValue(study['0008,0030']),
accessionNumber: remoteGetValue(study['0008,0050']),
referringPhysicianName: remoteGetValue(study['0008,0090']),
// 00081190 = URL
patientName: remoteGetValue(study['0010,0010']),
patientId: remoteGetValue(study['0010,0020']),
patientBirthdate: remoteGetValue(study['0010,0030']),
patientSex: remoteGetValue(study['0010,0040']),
studyId: remoteGetValue(study['0020,0010']),
numberOfStudyRelatedSeries: parseFloat(remoteGetValue(study['0020,1206'])),
numberOfStudyRelatedInstances: parseFloat(remoteGetValue(study['0020,1208'])),
studyDescription: remoteGetValue(study['0008,1030']),
modalities: remoteGetValue(study['0008,0061'])
});
});
return studies;
}
Services.REMOTE.Studies = function(server, filter) {
var parameters = {
PatientName: filter.patientName ? filter.patientName : "",
PatientID: filter.patientId,
AccessionNumber: filter.accessionNumber ? filter.accessionNumber : "",
StudyDescription: "",
StudyDate : "",
StudyTime : "",
ReferringPhysicianName : "",
PatientBirthDate : "",
PatientSex : "",
StudyID : "",
NumberOfStudyRelatedSeries : "",
NumberOfStudyRelatedInstances : "",
ModalitiesInStudy : ""
};
var remote = new OrthancRemote(server.root, server.sourceAE);
var data = remote.findStudies(server.modality, parameters);
return resultDataToStudies(data.results);
};
@@ -1,144 +0,0 @@
/**
* Creates a URL for a WADO search
*
* @param server
* @param studyInstanceUid
* @returns {string}
*/
function buildUrl(server, studyInstanceUid) {
return server.wadoRoot + '/studies/' + studyInstanceUid + '/metadata';
}
/**
* Parses the SourceImageSequence, if it exists, in order
* to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID
* is used to refer to this image in any accompanying DICOM-SR documents.
*
* @param instance
* @returns {String} The ReferenceSOPInstanceUID
*/
function getSourceImageInstanceUid(instance) {
// TODO= Parse the whole Source Image Sequence
// This is a really poor workaround for now.
// Later we should probably parse the whole sequence.
var SourceImageSequence = instance['00082112'];
if (SourceImageSequence && SourceImageSequence.Value && SourceImageSequence.Value.length) {
return SourceImageSequence.Value[0]['00081155'].Value[0];
}
}
/**
* Parses result data from a WADO search into Study MetaData
* Returns an object populated with study metadata, including the
* series list.
*
* @param server
* @param studyInstanceUid
* @param resultData
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
*/
function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
var seriesMap = {};
var seriesList = [];
if (!resultData.length) {
return;
}
var anInstance = resultData[0];
if (!anInstance) {
return;
}
var studyData = {
seriesList: seriesList,
patientName: DICOMWeb.getName(anInstance['00100010']),
patientId: DICOMWeb.getString(anInstance['00100020']),
accessionNumber: DICOMWeb.getString(anInstance['00080050']),
studyDate: DICOMWeb.getString(anInstance['00080020']),
modalities: DICOMWeb.getString(anInstance['00080061']),
studyDescription: DICOMWeb.getString(anInstance['00081030']),
imageCount: DICOMWeb.getString(anInstance['00201208']),
studyInstanceUid: DICOMWeb.getString(anInstance['0020000D']),
};
resultData.forEach(function(instance) {
var seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
var series = seriesMap[seriesInstanceUid];
if (!series) {
series = {
seriesDescription: DICOMWeb.getString(instance['0008103E']),
modality: DICOMWeb.getString(instance['00080060']),
seriesInstanceUid: seriesInstanceUid,
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
instances: []
};
seriesMap[seriesInstanceUid] = series;
seriesList.push(series);
}
var sopInstanceUid = DICOMWeb.getString(instance['00080018']);
var instanceSummary = {
imageType: DICOMWeb.getString(instance['00080008']),
sopClassUid: DICOMWeb.getString(instance['00080016']),
sopInstanceUid: sopInstanceUid,
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
rows: DICOMWeb.getNumber(instance['00280010']),
columns: DICOMWeb.getNumber(instance['00280011']),
pixelSpacing: DICOMWeb.getString(instance['00280030']),
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
bitsStored: DICOMWeb.getNumber(instance['00280101']),
highBit: DICOMWeb.getNumber(instance['00280102']),
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
windowCenter: DICOMWeb.getString(instance['00281050']),
windowWidth: DICOMWeb.getString(instance['00281051']),
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
laterality: DICOMWeb.getString(instance['00200062']),
viewPosition: DICOMWeb.getString(instance['00185101']),
numFrames: DICOMWeb.getNumber(instance['00280008']),
frameTime: DICOMWeb.getNumber(instance['00181063']),
sliceThickness: DICOMWeb.getNumber(instance['00180050'])
};
if (server.imageRendering === 'wadouri') {
instanceSummary.wadouri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom";
} else {
instanceSummary.wadorsuri = server.wadoRoot + '/studies/' + studyInstanceUid + '/series/' + seriesInstanceUid + '/instances/' + sopInstanceUid + '/frames/1';
}
series.instances.push(instanceSummary);
});
return studyData;
}
/**
* Retrieved Study MetaData from a DICOM server using a WADO call
* @param server
* @param studyInstanceUid
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
*/
Services.WADO.RetrieveMetadata = function(server, studyInstanceUid) {
var url = buildUrl(server, studyInstanceUid);
var result = DICOMWeb.getJSON(url, server.requestOptions);
var study = resultDataToStudyMetadata(server, studyInstanceUid, result.data);
if (!study) {
study = {};
}
study.wadoUriRoot = server.wadoUriRoot;
study.studyInstanceUid = studyInstanceUid;
return study;
};