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

@@ -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