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