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,75 @@
|
||||
MatchedProtocols = new Meteor.Collection(null);
|
||||
|
||||
Comparators = new Meteor.Collection(null);
|
||||
|
||||
var comparators = [{
|
||||
id: 'equals',
|
||||
name: '= (Equals)',
|
||||
validator: 'equals',
|
||||
validatorOption: 'value',
|
||||
description: 'The attribute must equal this value.'
|
||||
}, {
|
||||
id: 'doesNotEqual',
|
||||
name: '!= (Does not equal)',
|
||||
validator: 'doesNotEqual',
|
||||
validatorOption: 'value',
|
||||
description: 'The attribute must not equal this value.'
|
||||
}, {
|
||||
id: 'contains',
|
||||
name: 'Contains',
|
||||
validator: 'contains',
|
||||
validatorOption: 'value',
|
||||
description: 'The attribute must contain this value.'
|
||||
}, {
|
||||
id: 'doesNotContain',
|
||||
name: 'Does not contain',
|
||||
validator: 'doesNotContain',
|
||||
validatorOption: 'value',
|
||||
description: 'The attribute must not contain this value.'
|
||||
}, {
|
||||
id: 'onlyInteger',
|
||||
name: 'Only Integers',
|
||||
validator: 'numericality',
|
||||
validatorOption: 'onlyInteger',
|
||||
description: "Real numbers won't be allowed."
|
||||
}, {
|
||||
id: 'greaterThan',
|
||||
name: '> (Greater than)',
|
||||
validator: 'numericality',
|
||||
validatorOption: 'greaterThan',
|
||||
description: 'The attribute has to be greater than this value.'
|
||||
}, {
|
||||
id: 'greaterThanOrEqualTo',
|
||||
name: '>= (Greater than or equal to)',
|
||||
validator: 'numericality',
|
||||
validatorOption: 'greaterThanOrEqualTo',
|
||||
description: 'The attribute has to be at least this value.'
|
||||
}, {
|
||||
id: 'lessThanOrEqualTo',
|
||||
name: '<= (Less than or equal to)',
|
||||
validator: 'numericality',
|
||||
validatorOption: 'lessThanOrEqualTo',
|
||||
description: 'The attribute can be this value at the most.'
|
||||
}, {
|
||||
id: 'lessThan',
|
||||
name: '< (Less than)',
|
||||
validator: 'numericality',
|
||||
validatorOption: 'lessThan',
|
||||
description: 'The attribute has to be less than this value.'
|
||||
}, {
|
||||
id: 'odd',
|
||||
name: 'Odd',
|
||||
validator: 'numericality',
|
||||
validatorOption: 'odd',
|
||||
description: 'The attribute has to be odd.'
|
||||
}, {
|
||||
id: 'even',
|
||||
name: 'Even',
|
||||
validator: 'numericality',
|
||||
validatorOption: 'even',
|
||||
description: 'The attribute has to be even.'
|
||||
}];
|
||||
|
||||
comparators.forEach(function(item) {
|
||||
Comparators.insert(item);
|
||||
});
|
||||
+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
|
||||
@@ -0,0 +1,19 @@
|
||||
UI.registerHelper('viewportSettingsTypes', function() {
|
||||
return HP.viewportSettingsTypes;
|
||||
});
|
||||
|
||||
UI.registerHelper('toolSettingsTypes', function() {
|
||||
return HP.toolSettingsTypes;
|
||||
});
|
||||
|
||||
UI.registerHelper('studyAttributes', function() {
|
||||
return HP.studyAttributes;
|
||||
});
|
||||
|
||||
UI.registerHelper('seriesAttributes', function() {
|
||||
return HP.seriesAttributes;
|
||||
});
|
||||
|
||||
UI.registerHelper('instanceAttributes', function() {
|
||||
return HP.instanceAttributes;
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
function humanize(text) {
|
||||
var humanized = text.replace(/([A-Z])/g, ' $1'); // insert a space before all caps
|
||||
humanized = humanized.replace(/^./, function(str) { // uppercase the first character
|
||||
return str.toUpperCase();
|
||||
})
|
||||
return humanized;
|
||||
}
|
||||
|
||||
UI.registerHelper('displayConstraint', function(attribute, constraint) {
|
||||
if (!constraint) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!attribute) {
|
||||
return;
|
||||
}
|
||||
|
||||
var validatorType = Object.keys(constraint)[0];
|
||||
if (!attribute) {
|
||||
return;
|
||||
}
|
||||
|
||||
var validator = Object.keys(constraint[validatorType])[0];
|
||||
if (!attribute) {
|
||||
return;
|
||||
}
|
||||
|
||||
var value = constraint[validatorType][validator];
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
var comparator = validator;
|
||||
if (validator === 'value') {
|
||||
comparator = validatorType;
|
||||
}
|
||||
|
||||
return humanize(attribute) + ' ' + humanize(comparator).toLowerCase() + ' ' + value;
|
||||
});
|
||||
@@ -0,0 +1,685 @@
|
||||
// Define a global variable that will be used to refer to the Protocol Engine
|
||||
// It must be populated by HP.setEngine when the Viewer is initialized and a ProtocolEngine
|
||||
// is instantiated on top of the LayoutManager. If the global ProtocolEngine variable remains
|
||||
// undefined, none of the HangingProtocol functions will operate.
|
||||
ProtocolEngine = undefined;
|
||||
|
||||
/**
|
||||
* Sets the ProtocolEngine global given an instantiated ProtocolEngine. This is done so that
|
||||
* The functions in the package can depend on a ProtocolEngine variable, but this variable does
|
||||
* not have to be exported from the application level.
|
||||
*
|
||||
* (There may be a better way to do this, but for now this works with no real downside)
|
||||
*
|
||||
* @param protocolEngine An instantiated ProtocolEngine linked to a LayoutManager from the
|
||||
* Viewerbase package
|
||||
*/
|
||||
HP.setEngine = function(protocolEngine) {
|
||||
ProtocolEngine = protocolEngine;
|
||||
};
|
||||
|
||||
// Define an empty object to store callbacks that are used to retrieve custom attributes
|
||||
// The simplest example for a custom attribute is the Timepoint type (i.e. baseline or follow-up)
|
||||
// used in the LesionTracker application.
|
||||
//
|
||||
// Timepoint type can be obtained given a studyId, and this is done through a custom callback.
|
||||
// Developers can define attributes (i.e. attributeId = timepointType) with a name ('Timepoint Type')
|
||||
// and a callback function that is used to calculate them.
|
||||
//
|
||||
// The input to the callback, which is called during viewport-image matching rule evaluation
|
||||
// is the set of attributes that contains the specified attribute. In our example, timepointType is
|
||||
// linked to the study attributes, and so the inputs to the callback is an object containing
|
||||
// the study attributes.
|
||||
HP.CustomAttributeRetrievalCallbacks = {};
|
||||
|
||||
/**
|
||||
* Adds a custom attribute to be used in the HangingProtocol UI and matching rules, including a
|
||||
* callback that will be used to calculate the attribute value.
|
||||
*
|
||||
* @param attributeId The ID used to refer to the attribute (e.g. 'timepointType')
|
||||
* @param attributeName The name of the attribute to be displayed (e.g. 'Timepoint Type')
|
||||
* @param callback The function used to calculate the attribute value from the other attributes at its level (e.g. study/series/image)
|
||||
*/
|
||||
HP.addCustomAttribute = function(attributeId, attributeName, callback) {
|
||||
HP.CustomAttributeRetrievalCallbacks[attributeId] = {
|
||||
name: attributeName,
|
||||
callback: callback
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
// Define an empty object to store callbacks that are used to apply custom viewport settings
|
||||
// after a viewport is rendered.
|
||||
HP.CustomViewportSettings = {};
|
||||
|
||||
/**
|
||||
* Adds a custom setting that can be chosen in the HangingProtocol UI and applied to a Viewport
|
||||
*
|
||||
* @param settingId The ID used to refer to the setting (e.g. 'displayCADMarkers')
|
||||
* @param settingName The name of the setting to be displayed (e.g. 'Display CAD Markers')
|
||||
* @param options
|
||||
* @param callback A function to be run after a viewport is rendered with a series
|
||||
*/
|
||||
HP.addCustomViewportSetting = function(settingId, settingName, options, callback) {
|
||||
HP.CustomViewportSettings[settingId] = {
|
||||
id: settingId,
|
||||
text: settingName,
|
||||
options: options,
|
||||
callback: callback
|
||||
};
|
||||
};
|
||||
|
||||
Meteor.startup(function() {
|
||||
HP.addCustomViewportSetting('wlPreset', 'Window/Level Preset', Object.keys(OHIF.viewer.wlPresets), function(element, optionValue) {
|
||||
if (OHIF.viewer.wlPresets.hasOwnProperty(optionValue)) {
|
||||
applyWLPreset(optionValue, element);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Log decisions regarding matching
|
||||
HP.match = function(attributes, rules) {
|
||||
var options = {
|
||||
format: 'grouped'
|
||||
};
|
||||
|
||||
var score = 0;
|
||||
var details = {
|
||||
passed: [],
|
||||
failed: []
|
||||
};
|
||||
|
||||
var requiredFailed = false;
|
||||
|
||||
rules.forEach(rule => {
|
||||
var attribute = rule.attribute;
|
||||
|
||||
// If the attributes we are testing (e.g. study, series, or instance attributes) do
|
||||
// not contain the attribute specified in the rule, check whether or not they have been
|
||||
// defined in the CustomAttributeRetrievalCallbacks Object.
|
||||
|
||||
// TODO: Investigate why attributes.hasOwnProperty(attribute) doesn't work?
|
||||
if (attributes[attribute] === undefined &&
|
||||
HP.CustomAttributeRetrievalCallbacks.hasOwnProperty(attribute)) {
|
||||
var customAttribute = HP.CustomAttributeRetrievalCallbacks[attribute];
|
||||
attributes[attribute] = customAttribute.callback(attributes);
|
||||
}
|
||||
|
||||
// Format the constraint as required by Validate.js
|
||||
var testConstraint = {};
|
||||
testConstraint[attribute] = rule.constraint;
|
||||
|
||||
// Use Validate.js to evaluate the constraints on the specified attributes
|
||||
var errorMessages = validate(attributes, testConstraint, [options]);
|
||||
|
||||
if (!errorMessages) {
|
||||
// If no errorMessages were returned, then validation passed.
|
||||
|
||||
// Add the rule's weight to the total score
|
||||
score += rule.weight;
|
||||
|
||||
// Log that this rule passed in the matching details object
|
||||
details.passed.push({
|
||||
rule: rule
|
||||
});
|
||||
} else {
|
||||
// If errorMessages were present, then validation failed
|
||||
|
||||
// If the rule that failed validation was Required, then
|
||||
// mark that a required Rule has failed
|
||||
if (rule.required) {
|
||||
requiredFailed = true;
|
||||
}
|
||||
|
||||
// Log that this rule failed in the matching details object
|
||||
// and include any error messages
|
||||
details.failed.push({
|
||||
rule: rule,
|
||||
errorMessages: errorMessages
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// If a required Rule has failed Validation, set the matching score to zero
|
||||
if (requiredFailed) {
|
||||
score = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
score: score,
|
||||
details: details
|
||||
};
|
||||
};
|
||||
|
||||
var sortByScore = function(arr) {
|
||||
arr.sort(function(a, b) {
|
||||
return b.score - a.score;
|
||||
});
|
||||
};
|
||||
|
||||
HP.ProtocolEngine = class ProtocolEngine {
|
||||
constructor(LayoutManager, studies) {
|
||||
this.LayoutManager = LayoutManager;
|
||||
this.studies = studies;
|
||||
|
||||
this.reset();
|
||||
|
||||
// Create an array for new stage ids to be stored
|
||||
// while editing a stage
|
||||
this.newStageIds = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the ProtocolEngine to the best match
|
||||
*/
|
||||
reset() {
|
||||
var protocol = this.getBestMatch();
|
||||
this.setHangingProtocol(protocol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current Stage from the current Protocol and stage index
|
||||
*
|
||||
* @returns {*} The Stage model for the currently displayed Stage
|
||||
*/
|
||||
getCurrentStageModel() {
|
||||
return this.protocol.stages[this.stage];
|
||||
}
|
||||
|
||||
findMatchByStudy(study) {
|
||||
var matched = [];
|
||||
|
||||
HangingProtocols.find().forEach(protocol => {
|
||||
// Clone the protocol's protocolMatchingRules array
|
||||
// We clone it so that we don't accidentally add the
|
||||
// numberOfPriorsReferenced rule to the Protocol itself.
|
||||
var rules = protocol.protocolMatchingRules.slice(0);
|
||||
if (!rules) {
|
||||
return;
|
||||
}
|
||||
|
||||
study.numberOfPriorsReferenced = this.getNumberOfAvailablePriors(study);
|
||||
var rule = new HP.ProtocolMatchingRule('numberOfPriorsReferenced', {
|
||||
numericality: {
|
||||
greaterThanOrEqualTo: protocol.numberOfPriorsReferenced
|
||||
}
|
||||
});
|
||||
|
||||
rules.push(rule);
|
||||
|
||||
var matchedDetails = HP.match(study, rules);
|
||||
|
||||
if (matchedDetails.score > 0) {
|
||||
matched.push({
|
||||
score: matchedDetails.score,
|
||||
protocol: protocol
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!matched.length) {
|
||||
var defaultProtocol = HangingProtocols.findOne({
|
||||
id: 'defaultProtocol'
|
||||
});
|
||||
|
||||
return [{
|
||||
score: 1,
|
||||
protocol: defaultProtocol
|
||||
}];
|
||||
}
|
||||
|
||||
sortByScore(matched);
|
||||
return matched;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the MatchedProtocols Collection by running the matching procedure
|
||||
*/
|
||||
updateMatches() {
|
||||
// Clear all data from the MatchedProtocols Collection
|
||||
MatchedProtocols.remove({});
|
||||
|
||||
// For each study, find the matching protocols
|
||||
this.studies.forEach(study => {
|
||||
var matched = this.findMatchByStudy(study);
|
||||
|
||||
// For each matched protocol, check if it is already in MatchedProtocols
|
||||
matched.forEach(function(matchedDetail) {
|
||||
var protocol = matchedDetail.protocol;
|
||||
var protocolInCollection = MatchedProtocols.findOne({
|
||||
id: protocol.id
|
||||
});
|
||||
|
||||
// If it is not already in the MatchedProtocols Collection, insert it
|
||||
if (!protocolInCollection) {
|
||||
MatchedProtocols.insert(protocol);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the best matched Protocol to the current study or set of studies
|
||||
* @returns {*}
|
||||
*/
|
||||
getBestMatch() {
|
||||
// Run the matching to populate the MatchedProtocols Collection
|
||||
this.updateMatches();
|
||||
|
||||
// Retrieve the highest scoring Protocol
|
||||
var sorted = MatchedProtocols.find({}, {
|
||||
sort: {
|
||||
score: -1
|
||||
},
|
||||
limit: 1
|
||||
}).fetch();
|
||||
|
||||
// Return the highest scoring Protocol
|
||||
return sorted[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the number of previous studies in the cached Worklist that
|
||||
* have the same patientId and an earlier study date
|
||||
*
|
||||
* @param study The input study
|
||||
* @returns {any|*} The number of available prior studies with the same patientId
|
||||
*/
|
||||
getNumberOfAvailablePriors(study) {
|
||||
var studies = WorklistStudies.find({
|
||||
patientId: study.patientId,
|
||||
studyDate: {
|
||||
$lt: study.studyDate
|
||||
}
|
||||
});
|
||||
|
||||
return studies.count();
|
||||
}
|
||||
|
||||
findRelatedStudies(protocol, study) {
|
||||
if (!protocol.protocolMatchingRules) {
|
||||
return;
|
||||
}
|
||||
|
||||
var studies = WorklistStudies.find({
|
||||
patientId: study.patientId,
|
||||
studyDate: {
|
||||
$lt: study.studyDate
|
||||
}
|
||||
}, {
|
||||
sort: {
|
||||
studyDate: -1
|
||||
}
|
||||
});
|
||||
|
||||
var related = [];
|
||||
var currentDate = moment(study.studyDate, 'YYYYMMDD');
|
||||
|
||||
studies.forEach(function(priorStudy, priorIndex) {
|
||||
// Calculate an abstract prior value for the study in question
|
||||
if (priorIndex === (studies.length - 1)) {
|
||||
priorStudy.abstractPriorValue = -1;
|
||||
} else {
|
||||
// Abstract prior index starts from 1 in the DICOM standard
|
||||
priorStudy.abstractPriorValue = priorIndex + 1;
|
||||
}
|
||||
|
||||
// Calculate the relative time using Moment.js
|
||||
var priorDate = moment(priorStudy.studyDate, 'YYYYMMDD');
|
||||
priorStudy.relativeTime = currentDate.diff(priorDate);
|
||||
|
||||
var details = HP.match(priorStudy, protocol.protocolMatchingRules);
|
||||
if (details.score) {
|
||||
related.push({
|
||||
score: details.score,
|
||||
study: priorStudy
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
sortByScore(related);
|
||||
return related.map(function(v) {
|
||||
return v.study;
|
||||
});
|
||||
}
|
||||
|
||||
// Match images given a list of Studies and a Viewport's image matching reqs
|
||||
matchImages(viewport) {
|
||||
var studyMatchingRules = viewport.studyMatchingRules;
|
||||
var seriesMatchingRules = viewport.seriesMatchingRules;
|
||||
var instanceMatchingRules = viewport.imageMatchingRules;
|
||||
|
||||
var highestStudyMatchingScore = 0;
|
||||
var highestSeriesMatchingScore = 0;
|
||||
var highestImageMatchingScore = 0;
|
||||
var matchingScores = [];
|
||||
var bestMatch;
|
||||
|
||||
var currentStudy = this.studies[0];
|
||||
currentStudy.abstractPriorValue = 0;
|
||||
|
||||
studyMatchingRules.forEach(rule => {
|
||||
if (rule.attribute === 'abstractPriorValue') {
|
||||
var validatorType = Object.keys(rule.constraint)[0];
|
||||
var validator = Object.keys(rule.constraint[validatorType])[0];
|
||||
var abstractPriorValue = rule.constraint[validatorType][validator];
|
||||
abstractPriorValue = parseInt(abstractPriorValue, 10);
|
||||
// TODO: Restrict or clarify validators for abstractPriorValue?
|
||||
|
||||
var studies = WorklistStudies.find({
|
||||
patientId: currentStudy.patientId,
|
||||
studyDate: {
|
||||
$lt: currentStudy.studyDate
|
||||
}
|
||||
}, {
|
||||
sort: {
|
||||
studyDate: -1
|
||||
}
|
||||
}).fetch();
|
||||
|
||||
// TODO: Revisit this later: What about two studies with the same
|
||||
// study date?
|
||||
|
||||
var priorStudy;
|
||||
if (abstractPriorValue === -1) {
|
||||
priorStudy = studies[studies.length - 1];
|
||||
} else {
|
||||
var studyIndex = Math.max(abstractPriorValue - 1, 0);
|
||||
priorStudy = studies[studyIndex];
|
||||
}
|
||||
|
||||
if (!priorStudy) {
|
||||
return;
|
||||
}
|
||||
|
||||
var alreadyLoaded = ViewerStudies.findOne({
|
||||
studyInstanceUid: priorStudy.studyInstanceUid
|
||||
});
|
||||
|
||||
if (!alreadyLoaded) {
|
||||
getStudyMetadata(priorStudy.studyInstanceUid, study => {
|
||||
study.abstractPriorValue = abstractPriorValue;
|
||||
|
||||
ViewerStudies.insert(study);
|
||||
this.studies.push(study);
|
||||
this.matchImages(viewport);
|
||||
this.updateViewports();
|
||||
});
|
||||
}
|
||||
}
|
||||
// TODO: Add relative Date / time
|
||||
});
|
||||
|
||||
var lastStudyIndex = this.studies.length - 1;
|
||||
this.studies.forEach(function(study) {
|
||||
var studyMatchDetails = HP.match(study, studyMatchingRules);
|
||||
if ((studyMatchingRules.length && !studyMatchDetails.score) ||
|
||||
studyMatchDetails.score < highestStudyMatchingScore) {
|
||||
return;
|
||||
}
|
||||
|
||||
highestStudyMatchingScore = studyMatchDetails.score;
|
||||
|
||||
study.seriesList.forEach(function(series) {
|
||||
var seriesMatchDetails = HP.match(series, seriesMatchingRules);
|
||||
if ((seriesMatchingRules.length && !seriesMatchDetails.score) ||
|
||||
seriesMatchDetails.score < highestSeriesMatchingScore) {
|
||||
return;
|
||||
}
|
||||
|
||||
highestSeriesMatchingScore = seriesMatchDetails.score;
|
||||
|
||||
series.instances.forEach(function(instance, index) {
|
||||
// This tests to make sure there is actually image data in this instance
|
||||
// TODO: Change this when we add PDF and MPEG support
|
||||
// See https://ohiforg.atlassian.net/browse/LT-227
|
||||
if (!instance.rows || !instance.columns) {
|
||||
return;
|
||||
}
|
||||
|
||||
var instanceMatchDetails = HP.match(instance, instanceMatchingRules);
|
||||
|
||||
var matchDetails = {
|
||||
passed: [],
|
||||
failed: []
|
||||
};
|
||||
|
||||
matchDetails.passed = matchDetails.passed.concat(instanceMatchDetails.details.passed);
|
||||
matchDetails.passed = matchDetails.passed.concat(seriesMatchDetails.details.passed);
|
||||
matchDetails.passed = matchDetails.passed.concat(studyMatchDetails.details.passed);
|
||||
|
||||
matchDetails.failed = matchDetails.failed.concat(instanceMatchDetails.details.failed);
|
||||
matchDetails.failed = matchDetails.failed.concat(seriesMatchDetails.details.failed);
|
||||
matchDetails.failed = matchDetails.failed.concat(studyMatchDetails.details.failed);
|
||||
|
||||
var totalMatchScore = instanceMatchDetails.score + seriesMatchDetails.score + studyMatchDetails.score;
|
||||
|
||||
var imageDetails = {
|
||||
studyInstanceUid: study.studyInstanceUid,
|
||||
seriesInstanceUid: series.seriesInstanceUid,
|
||||
sopInstanceUid: instance.sopInstanceUid,
|
||||
currentImageIdIndex: index,
|
||||
matchingScore: totalMatchScore,
|
||||
matchDetails: matchDetails
|
||||
};
|
||||
|
||||
if ((totalMatchScore > highestImageMatchingScore) || !bestMatch) {
|
||||
highestImageMatchingScore = totalMatchScore;
|
||||
bestMatch = imageDetails;
|
||||
}
|
||||
|
||||
matchingScores.push(imageDetails);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
bestMatch: bestMatch,
|
||||
matchingScores: matchingScores
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rerenders viewports that are part of the current ProtocolEngine's LayoutManager
|
||||
* using the matching rules internal to each viewport.
|
||||
*
|
||||
* If this function is provided the index of a viewport, only the specified viewport
|
||||
* is rerendered.
|
||||
*
|
||||
* @param viewportIndex
|
||||
*/
|
||||
updateViewports(viewportIndex) {
|
||||
// Make sure we have an active protocol with a non-empty array of display sets
|
||||
if (!this.protocol || !this.protocol.stages || !this.protocol.stages.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the current display set in the display set sequence
|
||||
var stageModel = this.getCurrentStageModel();
|
||||
|
||||
// If the current display set does not fulfill the requirements to be displayed,
|
||||
// stop here.
|
||||
if (!stageModel ||
|
||||
!stageModel.viewportStructure ||
|
||||
!stageModel.viewports ||
|
||||
!stageModel.viewports.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the layoutTemplate associated with the current display set's viewport structure
|
||||
// If no such template name exists, stop here.
|
||||
var layoutTemplateName = stageModel.viewportStructure.getLayoutTemplateName();
|
||||
if (!layoutTemplateName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the properties associated with the current display set's viewport structure template
|
||||
// If no such layout properties exist, stop here.
|
||||
var layoutProps = stageModel.viewportStructure.properties;
|
||||
if (!layoutProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an empty array to store the output viewportData
|
||||
var viewportData = [];
|
||||
|
||||
// Empty the matchDetails associated with the ProtocolEngine.
|
||||
// This will be used to store the pass/fail details and score
|
||||
// for each of the viewport matching procedures
|
||||
this.matchDetails = [];
|
||||
|
||||
// Loop through each viewport
|
||||
stageModel.viewports.forEach((viewport, viewportIndex) => {
|
||||
var details = this.matchImages(viewport);
|
||||
this.matchDetails[viewportIndex] = details;
|
||||
|
||||
// Convert any YES/NO values into true/false for Cornerstone
|
||||
var cornerstoneViewportParams = {};
|
||||
Object.keys(viewport.viewportSettings).forEach(function(key) {
|
||||
var value = viewport.viewportSettings[key];
|
||||
if (value === 'YES') {
|
||||
value = true;
|
||||
} else if (value === 'NO') {
|
||||
value = false;
|
||||
}
|
||||
|
||||
cornerstoneViewportParams[key] = value;
|
||||
});
|
||||
|
||||
// imageViewerViewports occasionally needs relevant layout data in order to set
|
||||
// the element style of the viewport in question
|
||||
var currentViewportData = $.extend({
|
||||
viewportIndex: viewportIndex,
|
||||
viewport: cornerstoneViewportParams
|
||||
}, layoutProps);
|
||||
|
||||
var customSettings = [];
|
||||
Object.keys(viewport.viewportSettings).forEach(id => {
|
||||
var setting = HP.CustomViewportSettings[id];
|
||||
if (!setting) {
|
||||
return;
|
||||
}
|
||||
|
||||
customSettings.push({
|
||||
id: id,
|
||||
value: viewport.viewportSettings[id]
|
||||
});
|
||||
});
|
||||
|
||||
currentViewportData.renderedCallback = function(element) {
|
||||
console.log('renderedCallback for ' + element.id);
|
||||
customSettings.forEach(function(customSetting) {
|
||||
console.log('Applying custom setting: ' + customSetting.id);
|
||||
console.log('with value: ' + customSetting.value);
|
||||
|
||||
var setting = HP.CustomViewportSettings[customSetting.id];
|
||||
setting.callback(element, customSetting.value);
|
||||
});
|
||||
};
|
||||
|
||||
if (details.bestMatch) {
|
||||
currentViewportData.studyInstanceUid = details.bestMatch.studyInstanceUid;
|
||||
currentViewportData.seriesInstanceUid = details.bestMatch.seriesInstanceUid;
|
||||
currentViewportData.sopInstanceUid = details.bestMatch.sopInstanceUid;
|
||||
currentViewportData.currentImageIdIndex = details.bestMatch.currentImageIdIndex;
|
||||
}
|
||||
|
||||
viewportData.push(currentViewportData);
|
||||
});
|
||||
|
||||
this.LayoutManager.layoutTempleName = layoutTemplateName;
|
||||
this.LayoutManager.layoutProps = layoutProps;
|
||||
this.LayoutManager.viewportData = viewportData;
|
||||
|
||||
if (viewportIndex !== undefined && viewportData[viewportIndex]) {
|
||||
this.LayoutManager.rerenderViewportWithNewSeries(viewportIndex, viewportData[viewportIndex]);
|
||||
} else {
|
||||
this.LayoutManager.updateViewports();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current Hanging Protocol to the specified Protocol
|
||||
* An optional argument can also be used to prevent the updating of the Viewports
|
||||
*
|
||||
* @param newProtocol
|
||||
* @param updateViewports
|
||||
*/
|
||||
setHangingProtocol(newProtocol, updateViewports=true) {
|
||||
// Reset the array of newStageIds
|
||||
this.newStageIds = [];
|
||||
|
||||
if (HP.Protocol.prototype.isPrototypeOf(newProtocol)) {
|
||||
this.protocol = newProtocol;
|
||||
} else {
|
||||
this.protocol = new HP.Protocol();
|
||||
this.protocol.fromObject(newProtocol);
|
||||
}
|
||||
|
||||
this.stage = 0;
|
||||
|
||||
// Update viewports by default
|
||||
if (updateViewports) {
|
||||
this.updateViewports();
|
||||
}
|
||||
|
||||
MatchedProtocols.update({}, {
|
||||
$set: {
|
||||
selected: false
|
||||
}
|
||||
}, {
|
||||
multi: true
|
||||
});
|
||||
|
||||
MatchedProtocols.update({
|
||||
id: this.protocol.id
|
||||
}, {
|
||||
$set: {
|
||||
selected: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the current stage to a new stage index in the display set sequence
|
||||
*
|
||||
* @param stage An integer value specifying the index of the desired Stage
|
||||
*/
|
||||
setCurrentProtocolStage(stage) {
|
||||
if (!this.protocol || !this.protocol.stages || !this.protocol.stages.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stage >= this.protocol.stages.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stage = stage;
|
||||
this.updateViewports();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the number of Stages in the current Protocol
|
||||
*/
|
||||
getNumProtocolStages() {
|
||||
if (!this.protocol || !this.protocol.stages || !this.protocol.stages.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.protocol.stages.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to the next protocol stage in the display set sequence
|
||||
*/
|
||||
nextProtocolStage() {
|
||||
this.setCurrentProtocolStage(++this.stage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to the previous protocol stage in the display set sequence
|
||||
*/
|
||||
previousProtocolStage() {
|
||||
this.setCurrentProtocolStage(--this.stage);
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user