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,20 @@
<template name="hangingProtocolButtons">
<button id="previousPresentationGroup" type="button"
class="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="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>
@@ -0,0 +1,73 @@
Template.hangingProtocolButtons.helpers({
/**
* Check if a later stage exists for the user to switch to
*
* @returns {boolean} Whether or not a later stage exists
*/
isNextAvailable: function() {
// Run this helper whenever the ProtocolEngine / LayoutManager has changed
Session.get('LayoutManagerUpdated');
// If no ProtocolEngine has been defined yet, stop here
if (!ProtocolEngine) {
return;
}
// Return whether or not the current stage is the last stage
return ProtocolEngine.stage < ProtocolEngine.getNumProtocolStages() - 1;
},
/**
* Check if an earlier stage exists for the user to switch to
*
* @returns {boolean} Whether or not an earlier stage exists
*/
isPreviousAvailable: function() {
// Run this helper whenever the ProtocolEngine / LayoutManager has changed
Session.get('LayoutManagerUpdated');
// If no ProtocolEngine has been defined yet, stop here
if (!ProtocolEngine) {
return;
}
// Return whether or not the current stage is the first stage
return ProtocolEngine.stage > 0;
}
});
Template.hangingProtocolButtons.events({
/**
* Switch to the previous Presentation group
*
* @param event The click event on the button
*/
'click #previousPresentationGroup': function(event) {
// If no ProtocolEngine has been defined yet, do nothing
if (!ProtocolEngine) {
return;
}
// Hide the button's Bootstrap tooltip in case it was shown
$(event.currentTarget).tooltip('hide');
// Instruct the ProtocolEngine to switch to the next stage
ProtocolEngine.previousProtocolStage();
},
/**
* Switch to the next Presentation group
*
* @param event The click event on the button
*/
'click #nextPresentationGroup': function(event) {
// If no ProtocolEngine has been defined yet, do nothing
if (!ProtocolEngine) {
return;
}
// Hide the button's Bootstrap tooltip in case it was shown
$(event.currentTarget).tooltip('hide');
// Instruct the ProtocolEngine to switch to the next stage
ProtocolEngine.nextProtocolStage();
}
});
@@ -0,0 +1,29 @@
<template name="matchedProtocols">
<div id='matchedProtocols' class="btn-group">
{{ #if matchedProtocols }}
<button type="button" class="btn btn-sm dropdown-toggle"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
title="Select Hanging Protocol">
<i class="fa fa-list-ol"></i><span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li>
<h5>Select a Hanging Protocol</h5>
</li>
{{ #each matchedProtocols }}
<li>
<a class='matchedProtocol {{#if active}}active{{/if}}'>{{name}}</a>
</li>
{{ /each }}
</ul>
{{ /if }}
<button id="toggleProtocolEditor" type="button" class="btn btn-sm"
title="Toggle Hanging Protocol Editor"
data-toggle="tooltip">
<i class="fa fa-cog"></i>
</button>
</div>
</template>
@@ -0,0 +1,50 @@
Template.matchedProtocols.onRendered(function() {
$('#matchedProtocols button').tooltip(OHIF.viewer.tooltipConfig);
});
Template.matchedProtocols.helpers({
/**
* Reactively re-render the MatchedProtocols Collection contents
*/
matchedProtocols: function() {
return MatchedProtocols.find();
}
});
Template.matchedProtocols.events({
/**
* Instruct the ProtocolEngine to apply the specified Hanging Protocol
*/
'click .matchedProtocol': function() {
var protocol = this;
ProtocolEngine.setHangingProtocol(protocol);
},
/**
* Show/hide the Protocol Editor sidebar
*/
'click #toggleProtocolEditor': function() {
// Select the Protocol Editor DOM element
var editor = $('#protocolEditor');
// Modulate the main viewer width in response to the sidebar being open
// This code is commented out so that the protocol editor is an overlay, rather than a sidebar
// This is something that is debatable at the moment, so I am not removing the code just yet
//
/* if (editor.hasClass('cbp-spmenu-open')) {
// Set the Viewer widths to reflect a hidden Protocol Editor
$('.viewerMain').width('calc(100% - 120px)');
$('.navbar').width('100%');
} else {
// Set the Viewer widths to reflect an open Protocol Editor
$('.viewerMain').width('calc(100% - 120px - 450px)');
$('.navbar').width('calc(100% - 450px)');
}*/
// Toggle a class reflect whether or not the Protocol Editor is displayed
editor.toggleClass('cbp-spmenu-open');
// Fire the resize handler after a short delay so that the animation can complete
setTimeout(handleResize, 150);
}
});
@@ -0,0 +1,14 @@
#matchedProtocols
ul.dropdown-menu
li
a
cursor: pointer
&:selected
color: #4fbfff
h5
display: block;
padding: 3px 20px;
clear: both;
line-height: 1.42857143;
@@ -0,0 +1,129 @@
<template name="protocolEditor">
{{ >ruleEntryDialog }}
{{ >settingEntryDialog }}
{{ >textEntryDialog }}
<div id='protocolEditor' class="cbp-spmenu cbp-spmenu-right">
<div class="row">
<div class="navigationButtons col-xs-12">
<ul class="nav nav-pills">
<li class="active">
<a data-toggle="tab" href="#protocolRulePane">
Protocol
</a>
</li>
<li>
<a data-toggle="tab" href="#activeViewportEditor">
Active Viewport
</a>
</li>
</ul>
</div>
</div>
<div class="tab-content">
<div id="protocolRulePane" class="tab-pane active">
{{#with activeProtocol}}
<div id="selectProtocol" class="protocolEditorSection">
<select id="protocolSelect" style="width: 50%">
</select>
<div class="dropdown protocolDropdown">
<button type="button" class="btn btn-sm dropdown-toggle" id="protocolDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="fa fa-cog"></i> <span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="protocolDropdown">
<li>
<a id="newProtocol">
New <i class="fa fa-file-text"></i>
</a>
</li>
<li>
<a class="btn-file">
Import <input type="file"> <i class="fa fa-upload"></i>
</a>
</li>
{{ #unless locked }}
<li>
<a id="renameProtocol">
Rename <i class="fa fa-pencil"></i>
</a>
</li>
{{ /unless }}
<li>
<a id="saveAsProtocol">
Save As <i class="fa fa-files-o"></i>
</a>
</li>
<li>
<a id="exportJSON">
Export <i class="fa fa-download"></i>
</a>
</li>
{{ #unless locked }}
<li>
<a id="deleteProtocol">
Delete <i class="fa fa-trash-o"></i>
</a>
</li>
{{ /unless }}
</ul>
</div>
</div>
<div id="editProtocol" class="protocolEditorSection">
<div class="row">
<div class="col-xs-12">
<p>Created: {{formatJSDate createdDate}}{{ #if createdBy }} by {{getUsername createdBy}}{{/if}}</p>
<p>Last modified: {{formatJSDate modifiedDate}}{{ #if createdBy }} by {{getUsername modifiedBy}}{{/if}}</p>
<p>Number of Priors Referenced: {{numberOfPriorsReferenced}}</p>
</div>
</div>
<div class="row">
<h3>Protocol Applicability</h3>
<hr/>
{{ >ruleTable level="protocol" rules=protocolMatchingRules attributes=studyAttributes}}
</div>
<div class="row">
<h3>Stages</h3>
<hr/>
{{ >stageSortable stages=stages}}
</div>
</div>
{{/with}}
</div>
<div id="activeViewportEditor" class="tab-pane">
{{ #if activeViewportUndefined }}
<div class="noActiveViewport">
<h2>Select a Viewport</h2>
<hr/>
<p>
Upon selecting a Viewport, this section will show a list of rules that can be used
to determine which images will populate the specified Viewport. You can specify these
rules to be evaluated at the study, series, or image level.
</p>
</div>
{{ /if }}
{{#with activeStage}}
{{ >stageDetails }}
{{/with}}
</div>
</div>
{{#with activeProtocol}}
<div id="protocolOptions" class="protocolEditorSection">
{{ #if locked }}
<button id="saveProtocol" type="button" class="btn btn-sm" disabled>
Locked <i class="fa fa-lock" aria-hidden="true"></i>
</button>
{{ else }}
<button id="saveProtocol" type="button" class="btn btn-sm">
Save Changes <i class="fa fa-floppy-o" aria-hidden="true"></i>
</button>
{{ /if }}
{{#if modifiedDate}}
<p class="lastSavedText">Last saved {{jsDateFromNow modifiedDate}}</p>
{{/if}}
</div>
<iframe id="download_iframe" style="display:none;"></iframe>
{{/with}}
</div>
</template>
@@ -0,0 +1,370 @@
/**
* Updates the Hanging Protocol Select2 Input
*/
function updateProtocolSelect() {
// Loop through the available HangingProtocols
// to create an array with the protocols that includes
// a property labelled 'text', so that Select2 has something
// to display
var protocols = HangingProtocols.find().map(function(protocol) {
protocol.text = protocol.name;
return protocol;
});
// Select the Protocol select DOM element
var protocolSelect = $('#protocolSelect');
// Empty the element using Select2 for rerendering
protocolSelect.select2().empty();
// Initialize the select element with Select2 using the
// array of protocols
protocolSelect.select2({
data: protocols
});
// Update the ProtocolSelector to display the current active Protocol
protocolSelect.select2().val(ProtocolEngine.protocol.id).trigger('change');
}
Template.protocolEditor.onRendered(function() {
this.timeAgoInterval = Meteor.setInterval(function() {
// Run this every minute
Session.set('timeAgoVariable', new Date());
}, 60000);
// Subscribe to the Hanging Protocols Collection
this.subscribe('hangingprotocols', function() {
// Update the Protocol select box when the Collection is ready
updateProtocolSelect();
});
});
Template.protocolEditor.onDestroyed(function() {
Meteor.clearInterval(this.timeAgoInterval);
});
Template.protocolEditor.helpers({
/**
* Reactively updates the active Protocol
*
* @returns {*} The currently active Protocol Model
*/
activeProtocol: function() {
// Whenever the Layout Manager is updated, trigger this helper
Session.get('LayoutManagerUpdated');
// If no ProtocolEngine, protocol, or stage is defined, stop here
if (!ProtocolEngine ||
!ProtocolEngine.protocol ||
ProtocolEngine.stage === undefined) {
return;
}
// Update the Protocol Select box
updateProtocolSelect();
// Find the protocol in the database
var protocolInDatabase = HangingProtocols.findOne({
id: ProtocolEngine.protocol.id
});
// Give the current Protocol an _id property from the Database
if (protocolInDatabase) {
ProtocolEngine.protocol._id = protocolInDatabase._id;
}
// Make sure that the number of referenced priors is correct
ProtocolEngine.protocol.updateNumberOfPriorsReferenced();
// Otherwise, return the active Hanging Protocol
return ProtocolEngine.protocol;
},
/**
* Reactively updates the active Protocol Stage
*
* @returns {*} The current Protocol's active Stage model
*/
activeStage: function() {
// Whenever the Layout Manager is updated, trigger this helper
Session.get('LayoutManagerUpdated');
// If no ProtocolEngine, protocol, or stage is defined, stop here
if (!ProtocolEngine ||
!ProtocolEngine.protocol ||
ProtocolEngine.stage === undefined) {
return;
}
// Retrieve the Stage Model for the current Protocol's active Stage
var stage = ProtocolEngine.getCurrentStageModel();
if (!stage) {
return;
}
// Update active Stage's layout template and properties based on the displayed
// layout properties. This is used to update the Stage Model when the user modifies
// the layout in the viewer
stage.viewportStructure.layoutTemplateName = layoutManager.layoutTemplateName;
stage.viewportStructure.properties = layoutManager.layoutProps;
// If there is a discrepancy between the Stage's number of viewports and the
// the number of required viewports given the properties above, rectify it
// by removing or adding Viewports to the stage
//
// First, calculate the difference, if any exists
var difference = stage.viewportStructure.getNumViewports() - stage.viewports.length;
if (difference < 0) {
// Make the viewport difference into a positive value
var absDifference = Math.abs(difference);
// If there are more Viewports defined than necessary, remove the extraneous Viewports
var position = stage.viewports.length - absDifference;
// Splice extra viewports from the Stage's viewports array
stage.viewports.splice(position, absDifference);
} else if (difference > 0) {
// If there are less Viewports defined than necessary, add viewports until we reach the
// required amount
// Count up until the difference in number of Viewports
for (var i = 0; i < difference; i++) {
// Instantiate a new Viewport Model
var viewport = new HP.Viewport();
// Add new Viewports to the Stage's viewports array
stage.viewports.push(viewport);
}
}
// Return the current Stage model for the active Protocol
return ProtocolEngine.getCurrentStageModel();
},
activeViewportUndefined: function() {
return (Session.get('activeViewport') === undefined);
}
});
Template.protocolEditor.events({
/**
* Creates a new Hanging Protocol and displays it in the Viewer
*/
'click #newProtocol': function() {
// Clone the default Protocol
var protocol = HP.defaultProtocol.createClone();
// Change the Protocol name to state that it is New, and give it a timestamp
protocol.name = 'New (created ' + moment().format('h:mm:ss a') + ')';
// Change the Protocol ID from the default value
protocol.id = uuid.new();
// Insert the Protocol into the HangingProtocols Collection
HangingProtocols.insert(protocol);
// Activate the new Protocol using the ProtocolEngine
ProtocolEngine.setHangingProtocol(protocol);
// Update the protocol selector to display the new Protocols
updateProtocolSelect();
},
/**
* Rename the current Protocol
*/
'click #renameProtocol': function() {
var selectedProtocol = this;
if (selectedProtocol.locked) {
return;
}
// Define some details for the text entry dialog
var title = 'Rename Protocol';
var instructions = 'Enter a new name';
var currentValue = selectedProtocol.name;
// Open the text entry dialog with the details above
// and fire the callback function when finished.
openTextEntryDialog(title, instructions, currentValue, function(value) {
// Update the name with the entered text
HangingProtocols.update(selectedProtocol._id, {
$set: {
name: value
}
});
selectedProtocol.name = value;
});
},
/**
* Triggers a custom event when for the HTML5 File input when files are selected
*
* @param event The Change event for the input
*/
'change .btn-file :file': function(event) {
// http://www.abeautifulsite.net/whipping-file-inputs-into-shape-with-bootstrap-3/
// Find the Input in the DOM
var input = $(event.currentTarget);
// Get the number of selected files
var numFiles = input.get(0).files ? input.get(0).files.length : 1;
// Get the label of the file
var label = input.val().replace(/\\/g, '/').replace(/.*\//, '');
// Trigger our custom event with the number of files and label
input.trigger('fileselect', [numFiles, label]);
},
/**
* Imports files selected by the user into the Hanging Protocols Collection
*
* @param event The custom fileselect event
*/
'fileselect .btn-file :file': function(event) {
// Retreieve the FileList
var files = event.target.files;
// Create an HTML5 File Reader
var reader = new FileReader();
reader.onload = function() {
var text = reader.result;
// POST the file to our protocol-import Route
// for it to be parsed and included in the
// HangingProtocols Collection
$.post('/protocol-import', {
protocol: text
}, function(resp, text, xhr) {
if (xhr.status !== 200) {
// If the server returns an error during importing, alert the user
// TODO: Use a custom dialog box, rather than "alert"
alert('Protocol import failed.');
}
});
};
// Instruct the FileReader to read the (first) selected file
// TODO: Update to allow batch uploads?
reader.readAsText(files[0], 'utf-8');
},
/**
* Set the Hanging Protocol when the select box is changed
*
* @param event The select2:select event
*/
'select2:select #protocolSelect': function(event) {
// Retrieve the protocolId
var protocolId = event.params.data.id;
// Retrieve the Protocol from the HangingProtocols Collection
var selectedProtocol = HangingProtocols.findOne({
id: protocolId
});
// If it doesn't exist, stop here
if (!selectedProtocol) {
return;
}
// Set the current Hanging Protocol to the user-specified Protocol
ProtocolEngine.setHangingProtocol(selectedProtocol);
},
/**
* Allow the Protocols / Stage navigation tabs to toggle the
* 'active' class when clicked
*/
'click .navigationButtons a': function() {
$(this).addClass('active').siblings().removeClass('active');
},
/**
* Update the HangingProtocols Collection with the latest changes to the current Protocol
*/
'click #saveProtocol': function() {
var selectedProtocol = this;
if (selectedProtocol.locked) {
return;
}
// Store the ID for the update call
var id = selectedProtocol._id;
// Remove the MongoDB _id property so that we can
// simplify the $set value
delete selectedProtocol._id;
// Update the Protocol's modifiedDate and modifiedBy User details
selectedProtocol.protocolWasModified();
// Update the current Protocol in the database with the latest changes
HangingProtocols.update(id, {
$set: selectedProtocol
});
},
/**
* Save the current Protocol as a new document in the HangingProtocols Collection
*/
'click #saveAsProtocol': function() {
var selectedProtocol = this;
// Define some details for the text entry dialog
var title = 'Save Protocol As';
var instructions = 'Enter a new name';
var currentValue = selectedProtocol.name;
// Open the text entry dialog with the details above
// and fire the callback function when finished.
openTextEntryDialog(title, instructions, currentValue, function(value) {
// Erase the MongoDB _id
delete selectedProtocol._id;
// Create a new ID for the protocol
selectedProtocol.id = uuid.new();
// Update the name with the entered text
selectedProtocol.name = value;
// Update the Protocol's modifiedDate and modifiedBy User details
selectedProtocol.protocolWasModified();
// Insert the new Protocol
HangingProtocols.insert(selectedProtocol);
});
},
/**
* Export the currently selected Protocol as a JSON file
*/
'click #exportJSON': function() {
// Tell the User's Browser to download the JSON file by routing a hidden iframe to our
// protocol-export Route. This prevents the tab from changing its current content.
var selectedProtocol = this;
document.getElementById('download_iframe').src = '/protocol-export/' + selectedProtocol.id;
},
/**
* Delete the currently selected Protocol
*/
'click #deleteProtocol': function() {
var selectedProtocol = this;
if (selectedProtocol.locked) {
return;
}
var options = {
title: 'Delete Protocol',
text: 'Are you sure you would like to remove this Protocol? This cannot be reversed.'
};
showConfirmDialog(function() {
// Send a call to remove the Protocol from the HangingProtocols Collection on the server
Meteor.call('removeHangingProtocol', selectedProtocol._id);
// Reset the ProtocolEngine to the next best match
ProtocolEngine.reset();
// Update the protocol selector
updateProtocolSelect();
}, options);
}
});
@@ -0,0 +1,154 @@
#protocolEditor
height: 100%
width: 450px
padding: 10px
position: absolute
top: 0
background: #212121
.navigationButtons
ul
margin: 0 auto
width: 300px
li
text-align: center
width: 150px
margin: 0
&:first-child:not(:last-child)
a
border-top-right-radius: 0
border-bottom-right-radius: 0
&:last-child:not(:first-child)
a
border-top-left-radius: 0
border-bottom-left-radius: 0
a
padding: 3px 9px
cursor: pointer
text-decoration: none
outline: none
background: #3e3e3e
color: #ffffff
-webkit-transition: all 0.3s ease
-moz-transition: all 0.3s ease
-o-transition: all 0.3s ease
-ms-transition: all 0.3s ease
transition: all 0.3s ease
&.active
a
background: #ffffff
color: #3e3e3e
p
h2
h3
h4
color: #f5f5f5
label
ul
color: #f5f5f5
font-weight: 400
label
margin-right: 10px
width: 25%
input[type='number']
input[type='text']
min-width: 50px
width: 40%
border: none
background: #212121
color: #ffffff
text-align: center
button.btn
background: #ffffff
color: #3e3e3e
.protocolEditorSection
padding: 10px
margin: 10px 0
#protocolOptions
text-align: center
position: absolute
bottom: 0
width: 100%
p
font-size: 8pt
color: #B3B3B3
margin: 0 10px
display: inline-block
#editProtocol
overflow-y: auto
overflow-x: hidden
padding-right: 16px
padding-left: 20px
margin-right: -16px
width: 100%
height: calc(100% - 120px)
&::-webkit-scrollbar
display: none
#selectProtocol
text-align: center
.btn-file
position: relative
overflow: hidden
.btn-file input[type=file]
position: absolute
top: 0
right: 0
min-width: 100%
min-height: 100%
font-size: 100px
text-align: right
filter: 'alpha(opacity=0)'
opacity: 0
outline: none
background: white
cursor: inherit
display: block
.protocolDropdown
display: inline-block
ul
li
a
cursor: pointer
#activeViewportEditor
padding: 10px
.noActiveViewport
padding: 40px
h3
color: #f5f5f5
.cbp-spmenu-right
right: -450px
.cbp-spmenu-right.cbp-spmenu-open
right: 0
/* Transitions */
.cbp-spmenu
-webkit-transition: all 0.3s ease
-moz-transition: all 0.3s ease
transition: all 0.3s ease
@@ -0,0 +1,47 @@
<template name="ruleEntryDialog">
<div class="ruleEntryDialog">
<div class="dialogHeader">
<h5>Rule Editor</h5>
</div>
<div class="dialogContent">
<div class="row">
<div class="col-xs-12">
<select class="attributes" style="width: 95%;">
</select>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<select class="comparators" style="width: 95%;">
{{ #each comparators }}
<option value={{id}}>{{name}}</option>
{{ /each }}
</select>
</div>
</div>
<div class="row">
<div class="col-xs-12">
{{ #if ifTypeIs currentValue "string"}}
<input class='currentValue' type="text" value={{currentValue}}>
{{ /if }}
{{ #if ifTypeIs currentValue "undefined"}}
<input class='currentValue' type="text" value={{currentValue}}>
{{ /if }}
{{ #if ifTypeIs currentValue "number"}}
<input class='currentValue' type="number" value={{currentValue}}>
{{ /if }}
{{ #if ifTypeIs currentValue "boolean"}}
<input class='currentValue' type="checkbox" {{inlineIf currentValue true "selected"}}>
{{ /if }}
</div>
</div>
</div>
<div class="dialogFooter">
<button id="cancel" class="btn btn-link" tabindex="1">Cancel</button>
<button id="save" class="btn btn-primary" tabindex="0">OK</button>
</div>
</div>
</template>
@@ -0,0 +1,395 @@
var keys = {
ESC: 27
};
/**
* Close the specified dialog element and return browser
* focus to the active viewport.
*
* @param dialog The DOM element of the dialog to close
*/
function closeHandler(dialog) {
// Hide the lesion dialog
$(dialog).css('display', 'none');
// Remove the backdrop
$('.removableBackdrop').remove();
// Restore the focus to the active viewport
setFocusToActiveViewport();
}
/**
* Displays and updates the UI of the Rule Entry Dialog given a new set of
* attributes, the rule level (protocol, study, series, or instance), and an
* optional rule to edit.
*
* @param attributes List of attributes the user can set
* @param level Level of the Rule to create / edit
* @param rule Optional Rule
*/
openRuleEntryDialog = function(attributes, level, rule) {
// Get the lesion location dialog
var dialog = $('.ruleEntryDialog');
// Clear any input that is still on the page
var currentValueInput = dialog.find('input.currentValue');
currentValueInput.val('');
// Store the Dialog DOM data, rule level and rule in the template data
Template.ruleEntryDialog.dialog = dialog;
Template.ruleEntryDialog.level = level;
Template.ruleEntryDialog.rule = rule;
// Initialize the Select2 search box for the attribute list
var attributeSelect = dialog.find('.attributes');
attributeSelect.html('').select2({
data: attributes,
placeholder: 'Select an attribute',
allowClear: true
});
// If a rule has been provided, set the value of the attribute Select2 input
// to the attribute set in the rule.
if (rule && rule.attribute) {
attributeSelect.val(rule.attribute);
}
// Trigger('change') is used to update the Select2 choice in the UI and so
// that the currentValue is updated based on the current attribute
attributeSelect.trigger('change');
// If a rule has been provided, use its constraint to find the relevant Comparator
if (rule && rule.constraint) {
var validator = Object.keys(rule.constraint)[0];
var validatorOption = Object.keys(rule.constraint[validator])[0];
var comparator = Comparators.findOne({
validator: validator,
validatorOption: validatorOption
});
// Set the current value input based on the rule constraint
var currentValue = rule.constraint[validator][validatorOption];
currentValueInput.val(currentValue);
}
// If a Comparator was found, set the default value of the Comparators select2 box
// to the comparatorId in the input rule
if (comparator) {
// Trigger('change') is used to update the Select2 choice in the UI
dialog.find('.comparators').val(comparator.id).trigger('change');
}
// Update the dialog's CSS so that it is visible on the page
dialog.css('display', 'block');
// Show the backdrop
UI.render(Template.removableBackdrop, document.body);
// Make sure the context menu is closed when the user clicks away
$('.removableBackdrop').one('mousedown touchstart', function() {
closeHandler(dialog);
});
};
/**
* Retrieves the current active element's imageId using Cornerstone
*/
function getActiveViewportImageId() {
// Retrieve the active viewport index from the Session
var activeViewport = Session.get('activeViewport');
if (activeViewport === undefined) {
return;
}
// Obtain the list of all Viewports on the page
var viewports = $('.imageViewerViewport');
// Retrieve the active viewport element
var element = viewports.get(activeViewport);
if (!element) {
return;
}
// Obtain the enabled element from Cornerstone
try {
var enabledElement = cornerstone.getEnabledElement(element);
if (!enabledElement) {
return;
}
} catch(error) {
log.warn(error);
return;
}
// Return the enabled element's imageId
return enabledElement.image.imageId;
}
function getAbstractPriorValue(imageId) {
var currentStudy = ViewerStudies.findOne({}, {
sort: {
studyDate: -1
},
limit: 1
});
if (!currentStudy) {
return;
}
var priorStudy = cornerstoneTools.metaData.get('study', imageId);
if (!priorStudy) {
return;
}
var studies = WorklistStudies.find({
patientId: currentStudy.patientId,
studyDate: {
$lt: currentStudy.studyDate
}
}, {
sort: {
studyDate: -1
}
});
var priorIndex = 0;
// TODO: Check what the abstract prior value should equal for an unrelated study?
studies.forEach(function(study, index) {
if (study.studyInstanceUid === priorStudy.studyInstanceUid) {
// Abstract prior index starts from 1 in the DICOM standard
// so we add 1 here
priorIndex = index + 1;
return false;
}
});
return priorIndex;
}
/**
* Retrieve the current value of an attribute
* @returns {*}
*/
function getCurrentAttributeValue(attribute, level) {
// Retrieve the active viewport's imageId. If none exists, stop here
var imageId = getActiveViewportImageId();
if (!imageId) {
return;
}
// If the dialog level is specified as 'protocol', change it to
// 'study' for metaData retrieval
if (level === 'protocol') {
level = 'study';
}
if (attribute === 'abstractPriorValue') {
return getAbstractPriorValue(imageId);
}
// Retrieve the metadata values for the specified level from
// the Cornerstone Tools metaData provider
var metadata = cornerstoneTools.metaData.get(level, imageId);
if (metadata[attribute] === undefined) {
return HP.attributeDefaults[attribute];
}
return metadata[attribute];
}
Template.ruleEntryDialog.onCreated(function() {
// Define the ReactiveVars that will be used to link aspects of the UI
var template = this;
// Note: currentValue's initial value must be a string so the template renders properly
template.currentValue = new ReactiveVar('');
template.attribute = new ReactiveVar();
template.comparatorId = new ReactiveVar();
});
Template.ruleEntryDialog.onRendered(function() {
// Initialize the Comparators Select2 box
var template = this;
template.$('.comparators').select2();
// Get the default Comparator from the Select2 box and use it to
// initialize the comparatorId ReactiveVar
var comparatorId = template.$('.comparators').val();
template.comparatorId.set(comparatorId);
});
Template.ruleEntryDialog.helpers({
/**
* Returns the Comparators Collection to the Template with reactive rerendering
*/
comparators: function() {
return Comparators.find();
},
/**
* Reactively updates the current value of the selected attribute for the selected image
*
* @returns {*} Attribute value for the active image
*/
currentValue: function() {
return Template.instance().currentValue.get();
}
});
Template.ruleEntryDialog.events({
/**
* Save a rule that is being edited
*
* @param event the Click event
* @param template The template context
*/
'click #save': function(event, template) {
// Retrieve the input properties to the template
var dialog = Template.ruleEntryDialog.dialog;
var level = Template.ruleEntryDialog.level;
// Retrieve the current values for the attribute value and comparatorId
var attribute = template.attribute.get();
var comparatorId = template.comparatorId.get();
var currentValue = template.currentValue.get();
// If currentValue input is undefined, prevent saving this rule
if (currentValue === undefined) {
return;
}
// Check if we are editing a rule or creating a new one
var rule;
if (Template.ruleEntryDialog.rule) {
// If we are editing a rule, change the rule data
rule = Template.ruleEntryDialog.rule;
} else {
// If we are creating a rule, obtain the active Viewport model
// from the Protocol and Stage
var viewport = getActiveViewportModel();
// Create a rule depending on the level property of this dialog
switch (level) {
case 'protocol':
rule = new HP.ProtocolMatchingRule();
ProtocolEngine.protocol.addProtocolMatchingRule(rule);
break;
case 'study':
rule = new HP.StudyMatchingRule();
viewport.studyMatchingRules.push(rule);
break;
case 'series':
rule = new HP.SeriesMatchingRule();
viewport.seriesMatchingRules.push(rule);
break;
case 'instance':
rule = new HP.ImageMatchingRule();
viewport.imageMatchingRules.push(rule);
break;
}
}
// Find the Comparator from the Comparators Collection given its ID
var comparator = Comparators.findOne({
id: comparatorId
});
// Create a new constraint to add to the rule
var constraint = {};
constraint[comparator.validator] = {};
constraint[comparator.validator][comparator.validatorOption] = currentValue;
// Set the attribute and constraint of the rule
rule.attribute = attribute;
rule.constraint = constraint;
// Instruct the Protocol Engine to update the Layout Manager with new data
var viewportIndex = Session.get('activeViewport');
ProtocolEngine.updateViewports(viewportIndex);
// Close the dialog
closeHandler(dialog);
},
/**
* Allow the user to click the Cancel button to close the dialog
*/
'click #cancel': function() {
var dialog = Template.ruleEntryDialog.dialog;
closeHandler(dialog);
},
/**
* Allow Esc keydown events to close the dialog
*
* @param event The Keydown event details
* @returns {boolean} Return false to prevent bubbling of the event
*/
'keydown .ruleEntryDialog': function(event) {
var dialog = Template.ruleEntryDialog.dialog;
// If Esc key is pressed, close the dialog
if (event.which === keys.ESC) {
closeHandler(dialog);
return false;
}
},
/**
* Update the currentValue ReactiveVar if the user changes the attribute
*
* @param event The Change event for the select box
* @param template The current template context
*/
'change select.attributes': function(event, template) {
// Obtain the user-specified attribute to test against
var attribute = $(event.currentTarget).val();
// Store it in the ReactiveVar
template.attribute.set(attribute);
// Store this attribute in the template data context
Template.ruleEntryDialog.selectedAttribute = attribute;
// Get the level of this dialog
var level = Template.ruleEntryDialog.level;
// Retrieve the current value of the attribute for the active viewport model
var value = getCurrentAttributeValue(attribute, level);
// Update the ReactiveVar with the user-specified value
template.currentValue.set(value);
},
/**
* Update the currentValue ReactiveVar if the user changes the attribute value
*
* @param event The Change event for the input
* @param template The current template context
*/
'change input.currentValue': function(event, template) {
// Get the DOM element representing the input box
var input = $(event.currentTarget);
// Get the current value of the input
var value = input.val();
// If the input is of type 'number', parse it as a Float
if (input.attr('type') === 'number') {
value = parseFloat(value);
}
// Update the ReactiveVar with the user-specified value
template.currentValue.set(value);
},
/**
* Update the comparatorId ReactiveVar whenever the Comparators select box is changed
*
* @param event The Change event for the select box
* @param template The current template context
*/
'change select.comparators': function(event, template) {
// Get the current value of the select box
var comparatorId = $(event.currentTarget).val();
// Update the ReactiveVar with the value of the Comparators select box
template.comparatorId.set(comparatorId);
}
});
@@ -0,0 +1,36 @@
.ruleEntryDialog
display: none
position: absolute
top: 0
bottom: 0
left: 0
right: 0
z-index: 100
width: 50%
max-width: 400px
height: 230px
margin: auto
border-radius: 5px
padding: 10px 20px 10px 20px
background-color: rgba(255,255,255,1)
outline: none
.dialogContent
text-align: center
margin-bottom: 10px
.row
margin: 15px 0
.btn
text-decoration: none
#cancel
float: left
#save
float: right
input.currentValue
width: 95%
text-align: center
@@ -0,0 +1,32 @@
<template name="ruleTable">
<table class="ruleTable table">
<thead>
<tr>
<th>Constraint</th>
<th>Wt.</th>
<th>Req.</th>
</tr>
</thead>
<tbody>
{{ #each rules }}
{{ #if constraint }}
<tr>
<td>
<i class="fa fa-pencil editRule"></i>
<i class="fa fa-trash deleteRule"></i>
{{displayConstraint attribute constraint}}
{{ #unless rulePassed }}<small class="failWarning">Fail</small>{{ /unless }}
</td>
<td><input class="ruleWeight" type="number" value={{weight}} min="1" step="1" max="1000"/></td>
<td><input class="ruleRequired" type="checkbox" {{inlineIf required true "checked"}}/></td>
</tr>
{{ /if }}
{{ /each }}
</tbody>
</table>
<div class="addRuleContainer">
<span class="addRule">
Add New Constraint <i class="fa fa-plus-circle"></i>
</span>
</div>
</template>
@@ -0,0 +1,130 @@
Template.ruleTable.helpers({
/**
* Retrieve validation data on each rule for the active viewport
*
* @returns {boolean} Whether or not the current rule passed for the active viewport
*/
rulePassed: function() {
// Retrieve the latest match details given the active viewport index
var viewportIndex = Session.get('activeViewport');
var details = ProtocolEngine.matchDetails[viewportIndex];
// If no match was found, stop here
if (!details || !details.bestMatch) {
return;
}
// Retrieve the list of failed rules for this Viewport
var failed = details.bestMatch.matchDetails.failed;
// Check if the current rule failed or not
var rule = this;
var hasPassed = true;
failed.forEach(function(failedRuleData) {
var failedRule = failedRuleData.rule;
if (failedRule.id === rule.id) {
hasPassed = false;
return false;
}
});
// Return a boolean representing whether or not the rule passed
return hasPassed;
}
});
Template.ruleTable.events({
/**
* Opens the Rule Entry dialog to allow the user to create a new rule
* Specifies attributes and rule level for the Rule Entry dialog
* based on the data given to this template.
*/
'click .addRule': function() {
// Get the current template data
var data = Template.currentData();
// Retrieve the rule attributes and level (e.g. study / series / instance)
var attributes = data.attributes;
var level = data.level;
// Open the Rule Entry Dialog with the attributes, level, and rule
openRuleEntryDialog(attributes, level);
},
/**
* Opens the Rule Entry dialog to allow the user to edit an existing
* rule. Passes rule details to the dialog so its current properties
* can be displayed.
*
* Specifies attributes and rule level for the Rule Entry dialog
* based on the data given to this template.
*/
'click .editRule': function() {
// Get the current template data
var data = Template.currentData();
// Retrieve the rule attribtes and level (e.g. study / series / instance)
var attributes = data.attributes;
var level = data.level;
// Get the properties of the current rule
var rule = this;
// Open the Rule Entry Dialog with the attributes, level, and rule
openRuleEntryDialog(attributes, level, rule);
},
/**
* Removes a rule from the current Viewport or Protocol depending on
* the type of rule
*/
'click .deleteRule': function() {
// Get the properties of the current rule
var rule = this;
if (rule instanceof HP.ProtocolMatchingRule) {
// If this Rule is evaluated at the protocol level,
// remove it from the current Protocol
ProtocolEngine.protocol.removeProtocolMatchingRule(rule);
} else {
// If this Rule is evaluated at the Viewport level,
// remove it from the active viewport model
var viewport = getActiveViewportModel();
viewport.removeRule(rule);
}
// Instruct the Protocol Engine to update the Layout Manager with new data
var viewportIndex = Session.get('activeViewport');
ProtocolEngine.updateViewports(viewportIndex);
},
/**
* Updates a Rule's weight in response to user input
*
* @param event The input change event
*/
'change .ruleWeight': function(event) {
// Get the properties of the current rule
var rule = this;
// Update the value of the rule weight
rule.weight = $(event.currentTarget).val();
// Instruct the Protocol Engine to update the Layout Manager with new data
var viewportIndex = Session.get('activeViewport');
ProtocolEngine.updateViewports(viewportIndex);
},
/**
* Updates a Rule's 'required' property in response to user input
*
* @param event The input change event
*/
'change .ruleRequired': function(event) {
// Get the properties of the current rule
var rule = this;
// Update the value of the 'required' property
rule.required = $(event.currentTarget).prop('checked');
// Instruct the Protocol Engine to update the Layout Manager with new data
var viewportIndex = Session.get('activeViewport');
ProtocolEngine.updateViewports(viewportIndex);
}
});
@@ -0,0 +1,46 @@
table.ruleTable
thead
tr
th
color: whitesmoke
text-align: center
th:first-child
text-align: left
tbody
tr
td
color: #ffffff
text-align: center
.failWarning
color: red
.editRule
.deleteRule
&:hover, &:active
cursor: pointer
color: #48d3ff
-webkit-transition: all 0.1s ease
-moz-transition: all 0.1s ease
transition: all 0.1s ease
td:first-child
text-align: left
.addRuleContainer
margin: 10px 0
text-align: center
color: #ffffff
.addRule
&:hover, &:active
cursor: pointer
color: #48d3ff
-webkit-transition: all 0.1s ease
-moz-transition: all 0.1s ease
transition: all 0.1s ease
@@ -0,0 +1,25 @@
<template name="settingEntryDialog">
<div class="settingEntryDialog">
<div class="dialogHeader">
<h5>Viewport Setting Editor</h5>
</div>
<div class="dialogContent">
<div class="row">
<div class="col-xs-12">
<select class="settings" style="width: 95%;">
</select>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<select class="currentValue" style="width: 95%;">
</select>
</div>
</div>
</div>
<div class="dialogFooter">
<button id="cancel" class="btn btn-link" tabindex="1">Cancel</button>
<button id="save" class="btn btn-primary" tabindex="0">OK</button>
</div>
</div>
</template>
@@ -0,0 +1,232 @@
var keys = {
ESC: 27
};
/**
* Close the specified dialog element and return browser
* focus to the active viewport.
*
* @param dialog The DOM element of the dialog to close
*/
function closeHandler(dialog) {
// Hide the lesion dialog
$(dialog).css('display', 'none');
// Remove the backdrop
$('.removableBackdrop').remove();
// Restore the focus to the active viewport
setFocusToActiveViewport();
}
/**
* Displays and updates the UI of the Setting Entry Dialog given an
* optional setting to edit.
*
* @param settingObject
*/
openSettingEntryDialog = function(settingObject) {
// Get the lesion location dialog
var dialog = $('.settingEntryDialog');
// Store the Dialog DOM data, setting level and setting in the template data
Template.settingEntryDialog.dialog = dialog;
Template.settingEntryDialog.settingObject = settingObject;
// Initialize the Select2 search box for the attribute list
var settings = Object.keys(HP.displaySettings);
settings.concat(Object.keys(HP.CustomViewportSettings));
var displaySettingsOptions = Object.keys(HP.displaySettings).map(key => {
return {
id: key,
text: HP.displaySettings[key].text
};
});
var customSettingsOptions = Object.keys(HP.CustomViewportSettings).map(key => {
return {
id: key,
text: HP.CustomViewportSettings[key].text
};
});
var settingsOptions = displaySettingsOptions.concat(customSettingsOptions);
var settingSelect = dialog.find('.settings');
settingSelect.html('').select2({
data: settingsOptions,
placeholder: 'Select a setting',
allowClear: true
});
var settingDetails = {
options: []
};
if (settingObject && HP.displaySettings[settingObject.id]) {
settingDetails = HP.displaySettings[settingObject.id];
} else if (settingObject && HP.CustomViewportSettings[settingObject.id]) {
settingDetails = HP.CustomViewportSettings[settingObject.id];
}
var valueSelect = dialog.find('.currentValue');
valueSelect.html('').select2({
data: settingDetails.options,
placeholder: 'Select a value',
allowClear: true
});
// If a setting has been provided, set the value of the attribute Select2 input
// to the attribute set in the setting.
if (settingObject && settingObject.id) {
settingSelect.val(settingObject.id);
}
// Trigger('change') is used to update the Select2 choice in the UI
// This is done after setting the value in case no setting was provided
settingSelect.trigger('change');
// If a setting has been provided, display its current value
if (settingObject && settingObject.value !== undefined) {
valueSelect.val(settingObject.value).trigger('change');
}
// Update the dialog's CSS so that it is visible on the page
dialog.css('display', 'block');
// Show the backdrop
Blaze.render(Template.removableBackdrop, document.body);
// Make sure the context menu is closed when the user clicks away
$('.removableBackdrop').one('mousedown touchstart', function() {
closeHandler(dialog);
});
};
Template.settingEntryDialog.onCreated(function() {
// Define the ReactiveVars that will be used to link aspects of the UI
var template = this;
// Note: currentValue's initial value must be a string so the template renders properly
template.currentValue = new ReactiveVar('');
template.setting = new ReactiveVar();
});
Template.settingEntryDialog.events({
/**
* Save a setting that is being edited
*
* @param event the Click event
* @param template The template context
*/
'click #save': function(event, template) {
// Retrieve the input properties to the template
var dialog = Template.settingEntryDialog.dialog;
// Retrieve the current values for the id and current value
var setting = template.setting.get();
var currentValue = template.currentValue.get();
// If currentValue input is undefined, prevent saving this setting
if (currentValue === undefined) {
return;
}
var viewportSetting = {
id: setting,
value: currentValue
};
// Obtain the active Viewport model from the Protocol and Stage
var viewport = getActiveViewportModel();
// Remove any old rules if the ID has been changes
var originalSettingObject = Template.settingEntryDialog.settingObject;
if (originalSettingObject && originalSettingObject.id) {
delete viewport.viewportSettings[originalSettingObject.id];
}
// Update the active Viewport model' viewportSettings dictionary
viewport.viewportSettings[viewportSetting.id] = viewportSetting.value;
// Instruct the Protocol Engine to update the Layout Manager with new data
var viewportIndex = Session.get('activeViewport');
ProtocolEngine.updateViewports(viewportIndex);
// Close the dialog
closeHandler(dialog);
},
/**
* Allow the user to click the Cancel button to close the dialog
*/
'click #cancel': function() {
var dialog = Template.settingEntryDialog.dialog;
closeHandler(dialog);
},
/**
* Allow Esc keydown events to close the dialog
*
* @param event The Keydown event details
* @returns {boolean} Return false to prevent bubbling of the event
*/
'keydown .settingEntryDialog': function(event) {
var dialog = Template.settingEntryDialog.dialog;
// If Esc key is pressed, close the dialog
if (event.which === keys.ESC) {
closeHandler(dialog);
return false;
}
},
/**
* Update the currentValue ReactiveVar if the user changes the attribute
*
* @param event The Change event for the select box
* @param template The current template context
*/
'change select.settings': function(event, template) {
// Obtain the user-specified attribute to test against
var settingId = $(event.currentTarget).val();
// Store it in the ReactiveVar
template.setting.set(settingId);
// Retrieve the current value from the attribute
var settingDetails = {
options: []
};
if (settingId && HP.displaySettings[settingId]) {
settingDetails = HP.displaySettings[settingId];
} else if (settingId && HP.CustomViewportSettings[settingId]) {
settingDetails = HP.CustomViewportSettings[settingId];
}
var dialog = Template.settingEntryDialog.dialog;
var valueSelect = dialog.find('.currentValue');
valueSelect.html('').select2({
data: settingDetails.options,
placeholder: 'Select a value',
allowClear: true
});
// Update the ReactiveVar with the user-specified value
if (settingDetails && settingDetails.defaultValue) {
template.currentValue.set(settingDetails.defaultValue);
valueSelect.val(settingDetails.defaultValue).trigger('change');
}
},
/**
* Update the currentValue ReactiveVar if the user changes the current value
*
* @param event The Change event for the input
* @param template The current template context
*/
'change select.currentValue': function(event, template) {
// Get the current value of the select box
var value = $(event.currentTarget).val();
// Update the ReactiveVar with the user-specified value
template.currentValue.set(value);
}
});
@@ -0,0 +1,36 @@
.settingEntryDialog
display: none
position: absolute
top: 0
bottom: 0
left: 0
right: 0
z-index: 100
width: 50%
max-width: 400px
height: 230px
margin: auto
border-radius: 5px
padding: 10px 20px 10px 20px
background-color: rgba(255,255,255,1)
outline: none
.dialogContent
text-align: center
margin-bottom: 10px
.row
margin: 15px 0
.btn
text-decoration: none
#cancel
float: left
#save
float: right
input.currentValue
width: 95%
text-align: center
@@ -0,0 +1,31 @@
<template name="settingsTable">
<table class="settingsTable table">
<thead>
<tr>
<th>Setting</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{{ #each objectToPairs settings }}
{{#if value}}
<tr>
<td>
<i class="fa fa-pencil editSetting"></i>
<i class="fa fa-trash deleteSetting"></i>
{{getSettingText}}
</td>
<td>
{{prettyPrintStringify value}}
</td>
</tr>
{{/if}}
{{ /each }}
</tbody>
</table>
<div class="addSettingContainer">
<span class="addSetting">
Add Display Setting <i class="fa fa-plus-circle"></i>
</span>
</div>
</template>
@@ -0,0 +1,57 @@
Template.settingsTable.events({
/**
* Opens the Setting Entry dialog to allow the user to create a new setting
* Specifies attributes and setting level for the Setting Entry dialog
* based on the data given to this template.
*/
'click .addSetting': function() {
// Open the Setting Entry Dialog
openSettingEntryDialog();
},
/**
* Opens the Setting Entry dialog to allow the user to edit an existing
* setting. Passes setting details to the dialog so its current properties
* can be displayed.
*
* Specifies attributes for the Setting Entry dialog
* based on the data given to this template.
*/
'click .editSetting': function() {
// Get the properties of the current setting
var setting = this;
// Open the Setting Entry Dialog with the setting
openSettingEntryDialog(setting);
},
/**
* Removes a setting from the current Viewport
*/
'click .deleteSetting': function() {
// Get the properties of the current setting
var setting = this;
// Retrieve the current viewport model
var viewport = getActiveViewportModel();
// Remove the specified setting
delete viewport.viewportSettings[setting.key];
// Instruct the Protocol Engine to update the Layout Manager with new data
var viewportIndex = Session.get('activeViewport');
ProtocolEngine.updateViewports(viewportIndex);
}
});
Template.settingsTable.helpers({
getSettingText: function() {
var setting = this;
if (HP.CustomViewportSettings[setting.key]) {
return HP.CustomViewportSettings[setting.key].text;
} else if (HP.displaySettings[setting.key]) {
return HP.displaySettings[setting.key].text;
} else {
return Blaze._globalHelpers['prettyPrintStringify'](setting.key);
}
}
});
@@ -0,0 +1,44 @@
table.settingsTable
thead
tr
th
color: whitesmoke
text-align: center
th:first-child
text-align: left
tbody
background: #3e3e3e
tr
td
color: #ffffff
text-align: center
.editSetting
.deleteSetting
&:hover, &:active
cursor:pointer
color: #48d3ff
-webkit-transition: all 0.1s ease
-moz-transition: all 0.1s ease
transition: all 0.1s ease
td:first-child
text-align: left
.addSettingContainer
margin: 10px 0
text-align: center
color: #ffffff
.addSetting
&:hover, &:active
cursor: pointer
color: #48d3ff
-webkit-transition: all 0.1s ease
-moz-transition: all 0.1s ease
transition: all 0.1s ease
@@ -0,0 +1,33 @@
<template name="stageDetails">
<div id="stageDetails" class="protocolEditorSection">
<div class="row">
<div class="col-xs-12">
{{ #with activeViewport }}
<div class="stageEditorSection">
<h3>Viewport Settings</h3>
<hr/>
{{ >settingsTable settings=viewportSettings}}
</div>
<div class="stageEditorSection">
<h3>Study Matching Rules</h3>
<hr/>
{{ >ruleTable level="study" rules=studyMatchingRules attributes=studyAttributes}}
</div>
<div class="stageEditorSection">
<h3>Series Matching Rules</h3>
<hr/>
{{ >ruleTable level="series" rules=seriesMatchingRules attributes=seriesAttributes}}
</div>
<div class="stageEditorSection">
<h3>Image Matching Rules</h3>
<hr/>
{{ >ruleTable level="instance" rules=imageMatchingRules attributes=instanceAttributes}}
</div>
{{ /with }}
</div>
</div>
</div>
</template>
@@ -0,0 +1,40 @@
getActiveViewportModel = function() {
// If no ProtocolEngine has been defined yet, or there is
// no currently displayed Protocol or Stage, stop here
if (!ProtocolEngine ||
!ProtocolEngine.protocol ||
ProtocolEngine.stage === undefined) {
return;
}
// Retrieve the model of the currently displayed stage
var stage = ProtocolEngine.getCurrentStageModel();
// Retrieve the index of the active viewport
var activeViewport = Session.get('activeViewport');
// If the active viewport index is outside the bounds of the
// number of Viewports defined for this Stage, stop here
if (activeViewport >= stage.viewports.length) {
return;
}
// Return the Viewport model for this viewport index in the
// current stage
return stage.viewports[activeViewport];
};
Template.stageDetails.helpers({
/**
* Retrieves the ViewportModel for the active viewport from the
* currently displayed Protocol and display sequence Stage
*
* @returns {*} The Viewport model for the active viewport
*/
activeViewport: function() {
// Run this function anytime the layout manager has changed
Session.get('LayoutManagerUpdated');
return getActiveViewportModel();
}
});
@@ -0,0 +1,29 @@
#stageDetails
overflow-y: auto
overflow-x: hidden
padding-right: 16px
padding-left: 20px
margin-right: -16px
width: 100%
height: 100%
&::-webkit-scrollbar
display: none
h3
label
color: #f5f5f5
label
margin-right: 10px
width: 25%
input
min-width: 50px
width: 40%
border: none
background: #3e3e3e
color: #ffffff
text-align: center
.stageEditorSection
margin: 30px 0
@@ -0,0 +1,30 @@
<template name="stageSortable">
<div id="stageSortingContainer">
<div class="moveStageButtons">
<a class="moveStageUp noselect" disabled="{{ #unless isPreviousAvailable }}true{{ /unless }}">
Move Stage Up <i class="fa fa-chevron-up"></i>
</a>
<a class="moveStageDown noselect" disabled="{{ #unless isNextAvailable }}true{{ /unless }}">
Move Stage Down <i class="fa fa-chevron-down"></i>
</a>
</div>
<div id="stageSortable" class="sortable">
{{#each stages}}
<div class="sortable-item" data-id={{id}}>
<span>
<div class="sortable-handle pull-left"></div>
<i class="deleteStage fa fa-trash fa-lg pull-right"></i>
<span class="{{#if isActiveStage}}active{{/if}}">
{{ stageLabel }}
</span>
</span>
</div>
{{/each}}
</div>
<div class="addStage">
<span id="addStage">
Create New Stage <i class="fa fa-plus-circle"></i>
</span>
</div>
</div>
</template>
@@ -0,0 +1,254 @@
/**
* Extend the Array prototype with a Swap function
* so we can swap stages more easily
*/
Array.prototype.move = function(oldIndex, newIndex) {
var value = this[oldIndex];
newIndex = Math.max(0, newIndex);
newIndex = Math.min(this.length, newIndex);
this.splice(oldIndex, 1);
this.splice(newIndex, 0, value);
return this;
};
/**
* Helper function to obtain the current index of a stage in the
* current protocol
*
* @param protocol The Hanging Protocol to search within
* @param id The id of the current stage to search for
* @returns {number} The index of the specified stage within the Protocol,
* or undefined if it is not present.
*/
function getStageIndex(protocol, id) {
var stageIndex;
protocol.stages.forEach(function(stage, index) {
if (stage.id === id) {
stageIndex = index;
return false;
}
});
return stageIndex;
}
Template.stageSortable.helpers({
/**
* Checks a specified stage to see if it is currently being displayed
*
* @returns {boolean} Whether or not the stage is currently being displayed
*/
isActiveStage: function() {
// Rerun this function every time the layout manager has been updated
Session.get('LayoutManagerUpdated');
// If no Protocol Engine has been defined yet, stop here to prevent errors
if (!ProtocolEngine) {
return;
}
var currentStage = ProtocolEngine.getCurrentStageModel();
if (!currentStage) {
return false;
}
// Return a boolean representing if the active stage and the specified stage index are equal
return (this.id === currentStage.id);
},
/**
* Retrieves the index of the stage at the point it was last saved
*
* @returns {number|*}
*/
stageLabel: function() {
var stage = this;
// If no Protocol Engine has been defined yet, stop here to prevent errors
if (!ProtocolEngine) {
return;
}
// Retrieve the last saved copy of the current protocol
var lastSavedCopy = HangingProtocols.findOne(ProtocolEngine.protocol._id);
// Try to find the index of this stage in the previously saved copy
var stageIndex = getStageIndex(lastSavedCopy, stage.id);
// If the stage is new, and therefore wasn't present in the last save,
// retrieve it's index in the array of new stage ids and use that for
// the label. Also include the time since it was created.
if (stageIndex === undefined) {
// Reactively update this helper every minute
Session.get('timeAgoVariable');
// Find the index of the stage in the array of newly created stage IDs
var newStageNumber = ProtocolEngine.newStageIds.indexOf(stage.id) + 1;
// Use Moment.js to format the createdDate of this stage relative to the
// current time
var dateCreatedFromNow = moment(stage.createdDate).fromNow();
// Return the label for the new stage,
// e.g. "New Stage 1 (created a few seconds ago)"
return 'New Stage ' + newStageNumber + ' (created ' + dateCreatedFromNow + ')';
}
// If the stage is not new, label it by the index it held in the stages array
// at the previous saved point
return 'Stage ' + ++stageIndex;
},
/**
* Check if a later stage exists for the user to switch to
*
* @returns {boolean} Whether or not a later stage exists
*/
isNextAvailable: function() {
// Run this helper whenever the ProtocolEngine / LayoutManager has changed
Session.get('LayoutManagerUpdated');
// If no ProtocolEngine has been defined yet, stop here
if (!ProtocolEngine) {
return;
}
// Return whether or not the current stage is the last stage
return ProtocolEngine.stage < ProtocolEngine.getNumProtocolStages() - 1;
},
/**
* Check if an earlier stage exists for the user to switch to
*
* @returns {boolean} Whether or not an earlier stage exists
*/
isPreviousAvailable: function() {
// Run this helper whenever the ProtocolEngine / LayoutManager has changed
Session.get('LayoutManagerUpdated');
// If no ProtocolEngine has been defined yet, stop here
if (!ProtocolEngine) {
return;
}
// Return whether or not the current stage is the first stage
return ProtocolEngine.stage > 0;
}
});
Template.stageSortable.events({
/**
* Displays a stage when its title is clicked
*/
'click .sortable-item span': function() {
// Retrieve the index of this stage in the display set sequences
var stageIndex = getStageIndex(ProtocolEngine.protocol, this.id);
// Display the selected stage
ProtocolEngine.setCurrentProtocolStage(stageIndex);
},
/**
* Creates a new stage and adds it to the currently loaded Protocol at
* the end of the display set sequence
*/
'click #addStage': function() {
// Retrieve the model describing the current stage
var stage = ProtocolEngine.getCurrentStageModel();
// Clone this stage to create a new stage
var newStage = stage.createClone();
// Remove the stage's name if it has one
delete newStage.name;
// Append this new stage to the end of the display set sequence
ProtocolEngine.protocol.stages.push(newStage);
// Append the new stage the list of new stage IDs, so we can label it properly
ProtocolEngine.newStageIds.push(newStage.id);
// Calculate the index of the last stage in the display set sequence
var stageIndex = ProtocolEngine.protocol.stages.length - 1;
// Switch to the last stage in the display set sequence
ProtocolEngine.setCurrentProtocolStage(stageIndex);
},
/**
* Deletes a stage from the currently loaded Protocol by removing it from
* the stages array. If it is the currently active stage, the current stage is
* set to one stage earlier in the display set sequence.
*/
'click .deleteStage': function() {
// If this is the only stage in the Protocol, stop here
if (ProtocolEngine.protocol.stages.length === 1) {
return;
}
var stageId = this.id;
var options = {
title: 'Remove Protocol Stage',
text: 'Are you sure you would like to remove this Protocol Stage? This cannot be reversed.'
};
showConfirmDialog(function() {
// Retrieve the index of this stage in the display set sequences
var stageIndex = getStageIndex(ProtocolEngine.protocol, stageId);
// Remove it from the display set sequence
ProtocolEngine.protocol.stages.splice(stageIndex, 1);
// If we have removed the currently active stage, switch to the one before it
if (ProtocolEngine.stage === stageIndex) {
// Make sure we don't try to switch to a stage index below zero
var newStageIndex = Math.max(stageIndex - 1, 0);
// Display the new stage
ProtocolEngine.setCurrentProtocolStage(newStageIndex);
}
// Update the Session variable to the UI re-renders
Session.set('LayoutManagerUpdated', Random.id());
}, options);
},
'click .moveStageUp': function() {
// Get the old and new indices following a 'sort' event
var oldIndex = ProtocolEngine.stage;
var newIndex = Math.max(ProtocolEngine.stage - 1, 0);
if (oldIndex === newIndex) {
return;
}
// Swap the stages in the current Protocol's display set sequence
// using our addition to the Array prototype
ProtocolEngine.protocol.stages = ProtocolEngine.protocol.stages.move(oldIndex, newIndex);
// If the currently displayed stage was reordered into a new position,
// update the value for the stage index in the displayed Protocol
ProtocolEngine.stage = newIndex;
// Update the Session variable to the UI re-renders
Session.set('LayoutManagerUpdated', Random.id());
},
'click .moveStageDown': function() {
// Get the old and new indices following a 'sort' event
var oldIndex = ProtocolEngine.stage;
var newIndex = Math.min(ProtocolEngine.stage + 1, ProtocolEngine.protocol.stages.length - 1);
if (oldIndex === newIndex) {
return;
}
// Swap the stages in the current Protocol's display set sequence
// using our addition to the Array prototype
ProtocolEngine.protocol.stages = ProtocolEngine.protocol.stages.move(oldIndex, newIndex);
// If the currently displayed stage was reordered into a new position,
// update the value for the stage index in the displayed Protocol
ProtocolEngine.stage = newIndex;
// Update the Session variable to the UI re-renders
Session.set('LayoutManagerUpdated', Random.id());
}
});
@@ -0,0 +1,72 @@
#stageSortingContainer
#stageSortable
.sortable-item
padding: 3px
span
color: #ffffff
cursor: pointer
&.active
color: #46e7ff
.sortable-handle
cursor: move
cursor: -webkit-grabbing
width: 15px
height: 15px
margin: 0 5px
filter: invert(100%)
-webkit-filter: invert(100%)
background-image: unquote("url(/packages/hangingprotocols/assets/dots.svg)")
.sortable.target
flex: 1 1 auto
margin-left: 1em
.sortable-ghost
opacity: 0.6
.deleteStage
cursor: pointer
color: white
-webkit-transition: all 0.1s ease
-moz-transition: all 0.1s ease
transition: all 0.1s ease
&:active, &:hover
color: #48d3ff
.addStage
margin: 10px 0
text-align: center
#addStage
cursor: pointer
color: #ffffff
-webkit-transition: all 0.1s ease
-moz-transition: all 0.1s ease
transition: all 0.1s ease
&:active, &:hover
color: #48d3ff
.moveStageButtons
margin: 10px 0
text-align: center
a
cursor: pointer
color: #ffffff
text-decoration: none
-webkit-transition: all 0.1s ease
-moz-transition: all 0.1s ease
transition: all 0.1s ease
&:active, &:hover
color: #48d3ff
&[disabled="true"]
opacity: 0.7
cursor: disabled
pointer-events: none
@@ -0,0 +1,23 @@
<template name="textEntryDialog">
<div class="textEntryDialog">
<div class="dialogHeader">
<h4 class="title">{{title}}}</h4>
</div>
<div class="dialogContent">
<div class="row">
<div class="col-xs-12">
<h5 class="instructions">{{instructions}}</h5>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<input class='currentValue' type="text" value={{currentValue}}>
</div>
</div>
</div>
<div class="dialogFooter">
<button class="cancel btn btn-link" tabindex="1">Cancel</button>
<button class="save btn btn-primary" tabindex="0">OK</button>
</div>
</div>
</template>
@@ -0,0 +1,132 @@
var keys = {
ESC: 27,
ENTER: 13
};
/**
* Close the specified dialog element and returns the browser
* focus to the active viewport.
*
* @param dialog The DOM element of the dialog to close
*/
function closeHandler(dialog) {
// Hide the lesion dialog
$(dialog).css('display', 'none');
// Remove the backdrop
$('.removableBackdrop').remove();
// Restore the focus to the active viewport
setFocusToActiveViewport();
}
/**
* Displays and updates the UI of the Text Entry Dialog given a new title,
* instructions, and doneCallback
*
* @param title Title of the dialog box
* @param instructions Instructions to display to the user
* @param doneCallback Function to execute when the dialog has been closed
*/
openTextEntryDialog = function(title, instructions, currentValue, doneCallback) {
// Get the lesion location dialog
var dialog = $('.textEntryDialog');
// Clear any input that is still on the page
var currentValueInput = dialog.find('input.currentValue');
currentValueInput.val(currentValue);
// Store the Dialog DOM data, rule level and rule in the template data
Template.textEntryDialog.dialog = dialog;
Template.textEntryDialog.title = title;
Template.textEntryDialog.instructions = instructions;
Template.textEntryDialog.doneCallback = doneCallback;
dialog.find('.title').html(title);
dialog.find('.instructions').html(instructions);
// Update the dialog's CSS so that it is visible on the page
dialog.css('display', 'block');
// Show the backdrop
UI.render(Template.removableBackdrop, document.body);
// Make sure the context menu is closed when the user clicks away
$('.removableBackdrop').one('mousedown touchstart', function() {
closeHandler(dialog);
});
};
Template.textEntryDialog.events({
/**
* Save the user-specified text
*
*/
'click .save': function() {
// Retrieve the input properties to the template
var dialog = Template.textEntryDialog.dialog;
var currentValue = dialog.find('input.currentValue').val();
// If currentValue input is undefined, prevent saving this rule
if (currentValue === undefined) {
return;
}
var doneCallback = Template.textEntryDialog.doneCallback;
if (doneCallback) {
doneCallback(currentValue);
}
// Close the dialog
closeHandler(Template.textEntryDialog.dialog);
},
/**
* Allow the user to click the Cancel button to close the dialog
*/
'click .cancel': function() {
closeHandler(Template.textEntryDialog.dialog);
},
/**
* Allow Esc keydown events to close the dialog
*
* @param event The Keydown event details
* @returns {boolean} Return false to prevent bubbling of the event
*/
'keydown .textEntryDialog': function(event) {
var dialog = Template.textEntryDialog.dialog;
// If Esc key is pressed, close the dialog
if (event.which === keys.ESC) {
closeHandler(dialog);
return false;
} else if (event.which === keys.ENTER) {
var currentValue = dialog.find('input.currentValue').val();
// If currentValue input is undefined, prevent saving this rule
if (currentValue === undefined) {
return;
}
var doneCallback = Template.textEntryDialog.doneCallback;
if (doneCallback) {
doneCallback(currentValue);
}
closeHandler(dialog);
return false;
}
},
/**
* Update the currentValue ReactiveVar if the user changes the attribute value
*
* @param event The Change event for the input
* @param template The current template context
*/
'change input.currentValue': function(event, template) {
// Get the DOM element representing the input box
var input = $(event.currentTarget);
// Update the template data with the current value
Template.textEntryDialog.currentValue = input.val();
}
});
@@ -0,0 +1,29 @@
.textEntryDialog
display: none
position: absolute
top: 0
bottom: 0
left: 0
right: 0
z-index: 100
width: 50%
max-width: 400px
height: 170px
margin: auto
border-radius: 5px
padding: 10px 20px 10px 20px
background-color: rgba(255,255,255,1)
outline: none
.dialogContent
margin-bottom: 10px
.cancel
float: left
.save
float: right
input.currentValue
width: 95%
text-align: left