First pass at a Hanging Protocol implementation and editor (LT-131)
This commit is contained in:
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
|
||||
Reference in new issue
Block a user