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
+20
@@ -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>
|
||||
+73
@@ -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
|
||||
+25
@@ -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);
|
||||
}
|
||||
});
|
||||
+36
@@ -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
|
||||
Reference in new issue
Block a user