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 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 75 75" version="1.1" x="0px" y="0px"><g><path style="" d="M 21.484375 11.132812 C 21.484375 16.777344 16.90625 21.355469 11.261719 21.355469 C 5.617188 21.355469 1.039062 16.777344 1.039062 11.132812 C 1.039062 5.488281 5.617188 0.910156 11.261719 0.910156 C 16.90625 0.910156 21.484375 5.488281 21.484375 11.132812 Z M 21.484375 11.132812 " stroke="none" fill-rule="nonzero" fill="rgb(0%,0%,0%)" fill-opacity="1"></path><path style="" d="M 47.722656 11.132812 C 47.722656 16.777344 43.144531 21.355469 37.5 21.355469 C 31.855469 21.355469 27.277344 16.777344 27.277344 11.132812 C 27.277344 5.488281 31.855469 0.910156 37.5 0.910156 C 43.144531 0.910156 47.722656 5.488281 47.722656 11.132812 Z M 47.722656 11.132812 " stroke="none" fill-rule="nonzero" fill="rgb(0%,0%,0%)" fill-opacity="1"></path><path style="" d="M 74.21875 11.132812 C 74.21875 16.777344 69.640625 21.355469 63.996094 21.355469 C 58.351562 21.355469 53.773438 16.777344 53.773438 11.132812 C 53.773438 5.488281 58.351562 0.910156 63.996094 0.910156 C 69.640625 0.910156 74.21875 5.488281 74.21875 11.132812 Z M 74.21875 11.132812 " stroke="none" fill-rule="nonzero" fill="rgb(0%,0%,0%)" fill-opacity="1"></path><path style="" d="M 21.355469 37.5 C 21.355469 43.144531 16.777344 47.71875 11.132812 47.71875 C 5.488281 47.71875 0.910156 43.144531 0.910156 37.5 C 0.910156 31.851562 5.488281 27.277344 11.132812 27.277344 C 16.777344 27.277344 21.355469 31.851562 21.355469 37.5 Z M 21.355469 37.5 " stroke="none" fill-rule="nonzero" fill="rgb(0%,0%,0%)" fill-opacity="1"></path><path style="" d="M 47.59375 37.5 C 47.59375 43.144531 43.015625 47.71875 37.371094 47.71875 C 31.726562 47.71875 27.148438 43.144531 27.148438 37.5 C 27.148438 31.851562 31.726562 27.277344 37.371094 27.277344 C 43.015625 27.277344 47.59375 31.851562 47.59375 37.5 Z M 47.59375 37.5 " stroke="none" fill-rule="nonzero" fill="rgb(0%,0%,0%)" fill-opacity="1"></path><path style="" d="M 74.089844 37.5 C 74.089844 43.144531 69.511719 47.71875 63.867188 47.71875 C 58.222656 47.71875 53.644531 43.144531 53.644531 37.5 C 53.644531 31.851562 58.222656 27.277344 63.867188 27.277344 C 69.511719 27.277344 74.089844 31.851562 74.089844 37.5 Z M 74.089844 37.5 " stroke="none" fill-rule="nonzero" fill="rgb(0%,0%,0%)" fill-opacity="1"></path><path style="" d="M 21.484375 63.214844 C 21.484375 68.859375 16.90625 73.4375 11.261719 73.4375 C 5.617188 73.4375 1.039062 68.859375 1.039062 63.214844 C 1.039062 57.570312 5.617188 52.992188 11.261719 52.992188 C 16.90625 52.992188 21.484375 57.570312 21.484375 63.214844 Z M 21.484375 63.214844 " stroke="none" fill-rule="nonzero" fill="rgb(0%,0%,0%)" fill-opacity="1"></path><path style="" d="M 47.722656 63.214844 C 47.722656 68.859375 43.144531 73.4375 37.5 73.4375 C 31.855469 73.4375 27.277344 68.859375 27.277344 63.214844 C 27.277344 57.570312 31.855469 52.992188 37.5 52.992188 C 43.144531 52.992188 47.722656 57.570312 47.722656 63.214844 Z M 47.722656 63.214844 " stroke="none" fill-rule="nonzero" fill="rgb(0%,0%,0%)" fill-opacity="1"></path><path style="" d="M 74.21875 63.214844 C 74.21875 68.859375 69.640625 73.4375 63.996094 73.4375 C 58.351562 73.4375 53.773438 68.859375 53.773438 63.214844 C 53.773438 57.570312 58.351562 52.992188 63.996094 52.992188 C 69.640625 52.992188 74.21875 57.570312 74.21875 63.214844 Z M 74.21875 63.214844 " stroke="none" fill-rule="nonzero" fill="rgb(0%,0%,0%)" fill-opacity="1"></path></g></svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,10 @@
|
||||
HangingProtocols = new Meteor.Collection('hangingprotocols');
|
||||
|
||||
HangingProtocols.allow({
|
||||
insert: function() {
|
||||
return true;
|
||||
},
|
||||
update: function() {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,104 @@
|
||||
HP.attributeDefaults = {
|
||||
abstractPriorValue: 0
|
||||
};
|
||||
|
||||
HP.displaySettings = {
|
||||
invert: {
|
||||
id: 'invert',
|
||||
text: 'Show Grayscale Inverted',
|
||||
defaultValue: 'NO',
|
||||
options: ['YES', 'NO']
|
||||
}
|
||||
};
|
||||
|
||||
HP.studyAttributes = [{
|
||||
id: 'patientId',
|
||||
text: '(x00100020) Patient ID'
|
||||
}, {
|
||||
id: 'studyInstanceUid',
|
||||
text: '(x0020000d) Study Instance UID'
|
||||
}, {
|
||||
id: 'studyInstanceUid',
|
||||
text: '(x0020000d) Study Instance UID'
|
||||
}, {
|
||||
id: 'studyDate',
|
||||
text: '(x00080020) Study Date'
|
||||
}, {
|
||||
id: 'studyTime',
|
||||
text: '(x00080030) Study Time'
|
||||
}, {
|
||||
id: 'studyDescription',
|
||||
text: '(x00081030) Study Description'
|
||||
}, {
|
||||
id: 'abstractPriorValue',
|
||||
text: 'Abstract Prior Value'
|
||||
}];
|
||||
|
||||
HP.protocolAttributes = [{
|
||||
id: 'patientId',
|
||||
text: '(x00100020) Patient ID'
|
||||
}, {
|
||||
id: 'studyInstanceUid',
|
||||
text: '(x0020000d) Study Instance UID'
|
||||
}, {
|
||||
id: 'studyDate',
|
||||
text: '(x00080020) Study Date'
|
||||
}, {
|
||||
id: 'studyTime',
|
||||
text: '(x00080030) Study Time'
|
||||
}, {
|
||||
id: 'studyDescription',
|
||||
text: '(x00081030) Study Description'
|
||||
}, {
|
||||
id: 'anatomicRegion',
|
||||
text: 'Anatomic Region'
|
||||
}];
|
||||
|
||||
HP.seriesAttributes = [{
|
||||
id: 'seriesInstanceUid',
|
||||
text: '(x0020000e) Series Instance UID'
|
||||
}, {
|
||||
id: 'modality',
|
||||
text: '(x00080060) Modality'
|
||||
}, {
|
||||
id: 'seriesNumber',
|
||||
text: '(x00080060) Series Number'
|
||||
}, {
|
||||
id: 'seriesDescription',
|
||||
text: '(x0008103e) Series Description'
|
||||
}, {
|
||||
id: 'numImages',
|
||||
text: 'Number of Images'
|
||||
}];
|
||||
|
||||
HP.instanceAttributes = [{
|
||||
id: 'sopClassUid',
|
||||
text: 'SOP Class UID'
|
||||
}, {
|
||||
id: 'sopInstanceUid',
|
||||
text: 'SOP Instance UID'
|
||||
}, {
|
||||
id: 'viewPosition',
|
||||
text: 'View Position'
|
||||
}, {
|
||||
id: 'instanceNumber',
|
||||
text: 'Instance Number'
|
||||
}, {
|
||||
id: 'imageType',
|
||||
text: 'Image Type'
|
||||
}, {
|
||||
id: 'frameTime',
|
||||
text: 'Frame Time'
|
||||
}, {
|
||||
id: 'laterality',
|
||||
text: 'Laterality'
|
||||
}, {
|
||||
id: 'index',
|
||||
text: 'Image Index'
|
||||
}, {
|
||||
id: 'photometricInterpretation',
|
||||
text: 'Photometric Interpretation'
|
||||
}, {
|
||||
id: 'sliceThickness',
|
||||
text: 'Slice Thickness'
|
||||
}];
|
||||
@@ -0,0 +1 @@
|
||||
HP = {};
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
Router.route('/protocol-export/:_id', function() {
|
||||
var protocolId = this.params._id;
|
||||
var protocol = HangingProtocols.findOne({
|
||||
id: protocolId
|
||||
});
|
||||
|
||||
if (!protocol) {
|
||||
this.response.writeHead(404);
|
||||
|
||||
// This will not actually respond with a 404 because of https://github.com/iron-meteor/iron-router/issues/1055
|
||||
this.response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the MongoDB _id
|
||||
delete protocol._id;
|
||||
|
||||
var protocolJSON = JSON.stringify(protocol, null, 2),
|
||||
currentDate = new Date(),
|
||||
filename = protocol.name + '-' + (currentDate.getTime().toString()) + '.json';
|
||||
|
||||
this.response.writeHead(200, {
|
||||
'Content-Type': 'application/x-download',
|
||||
'Content-Disposition': 'attachment; filename=' + filename,
|
||||
'Content-Length': protocolJSON.length
|
||||
});
|
||||
|
||||
this.response.end(protocolJSON);
|
||||
}, {
|
||||
where: 'server'
|
||||
});
|
||||
|
||||
Router.route('/protocol-import', {
|
||||
where: 'server'
|
||||
}).post(function() {
|
||||
try {
|
||||
var toImport = JSON.parse(this.request.body.protocol);
|
||||
HangingProtocols.insert(toImport);
|
||||
|
||||
this.response.writeHead(200);
|
||||
this.response.end(toImport.id);
|
||||
} catch(e) {
|
||||
this.response.writeHead(500);
|
||||
this.response.end('Failed to parse protocol JSON.');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,599 @@
|
||||
/**
|
||||
* Removes the first instance of an element from an array, if an equal value exists
|
||||
*
|
||||
* @param array
|
||||
* @param input
|
||||
*
|
||||
* @returns {boolean} Whether or not the element was found and removed
|
||||
*/
|
||||
function removeFromArray(array, input) {
|
||||
// If the array is empty, stop here
|
||||
if (!array ||
|
||||
!array.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
array.forEach(function(value, index) {
|
||||
if (_.isEqual(value, input)) {
|
||||
indexToRemove = index;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (indexToRemove === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
array.splice(indexToRemove, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class represents a Hanging Protocol at the highest level
|
||||
*
|
||||
* @type {Protocol}
|
||||
*/
|
||||
HP.Protocol = class Protocol {
|
||||
/**
|
||||
* The Constructor for the Class to create a Protocol with the bare
|
||||
* minimum information
|
||||
*
|
||||
* @param name The desired name for the Protocol
|
||||
*/
|
||||
constructor(name) {
|
||||
// Create a new UUID for this Protocol
|
||||
this.id = uuid.new();
|
||||
|
||||
// Store a value which determines whether or not a Protocol is locked
|
||||
// This is probably temporary, since we will eventually have role / user
|
||||
// checks for editing. For now we just need it to prevent changes to the
|
||||
// default protocols.
|
||||
this.locked = false;
|
||||
|
||||
// Apply the desired name
|
||||
this.name = name;
|
||||
|
||||
// Set the created and modified dates to Now
|
||||
this.createdDate = new Date();
|
||||
this.modifiedDate = new Date();
|
||||
|
||||
// If we are logged in while creating this Protocol,
|
||||
// store this information as well
|
||||
if (Meteor.users && Meteor.userId) {
|
||||
this.createdBy = Meteor.userId;
|
||||
this.modifiedBy = Meteor.userId;
|
||||
}
|
||||
|
||||
// Create two empty Sets specifying which roles
|
||||
// have read and write access to this Protocol
|
||||
this.availableTo = new Set();
|
||||
this.editableBy = new Set();
|
||||
|
||||
// Define empty arrays for the Protocol matching rules
|
||||
// and Stages
|
||||
this.protocolMatchingRules = [];
|
||||
this.stages = [];
|
||||
this.numberOfPriorsReferenced = 0;
|
||||
}
|
||||
|
||||
updateNumberOfPriorsReferenced() {
|
||||
var numPriorsReferenced = 0;
|
||||
|
||||
this.stages.forEach(function(stage) {
|
||||
if (!stage.viewports) {
|
||||
return;
|
||||
}
|
||||
|
||||
stage.viewports.forEach(function(viewport) {
|
||||
if (!viewport.studyMatchingRules) {
|
||||
return;
|
||||
}
|
||||
|
||||
viewport.studyMatchingRules.forEach(function(rule) {
|
||||
if (rule.attribute === 'abstractPriorValue') {
|
||||
// TODO: Double check here that the abstractPriorValue is not
|
||||
// set as zero
|
||||
numPriorsReferenced++;
|
||||
} else if (rule.attribute === 'relativeTime') {
|
||||
numPriorsReferenced++;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.numberOfPriorsReferenced = numPriorsReferenced;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to update the modifiedDate when the Protocol
|
||||
* has been changed
|
||||
*/
|
||||
protocolWasModified() {
|
||||
// If we are logged in while modifying this Protocol,
|
||||
// store this information as well
|
||||
if (Meteor.users && Meteor.userId) {
|
||||
this.modifiedBy = Meteor.userId;
|
||||
}
|
||||
|
||||
this.updateNumberOfPriorsReferenced();
|
||||
|
||||
// Update the modifiedDate with the current Date/Time
|
||||
this.modifiedDate = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Occasionally the Protocol class needs to be instantiated from a JavaScript Object
|
||||
* containing the Protocol data. This function fills in a Protocol with the Object
|
||||
* data.
|
||||
*
|
||||
* @param input A Protocol as a JavaScript Object, e.g. retrieved from MongoDB or JSON
|
||||
*/
|
||||
fromObject(input) {
|
||||
// Check if the input already has an ID
|
||||
// If so, keep it. It not, create a new UUID
|
||||
this.id = input.id || uuid.new();
|
||||
|
||||
// Assign the input name to the Protocol
|
||||
this.name = input.name;
|
||||
|
||||
// Retrieve locked status, use !! to make it truthy
|
||||
// so that undefined values will be set to false
|
||||
this.locked = !!input.locked;
|
||||
|
||||
// TODO: Check how to regenerate Set from Object
|
||||
//this.availableTo = new Set(input.availableTo);
|
||||
//this.editableBy = new Set(input.editableBy);
|
||||
|
||||
// If the input contains Protocol matching rules
|
||||
if (input.protocolMatchingRules) {
|
||||
input.protocolMatchingRules.forEach(ruleObject => {
|
||||
// Create new Rules from the stored data
|
||||
var rule = new HP.ProtocolMatchingRule();
|
||||
rule.fromObject(ruleObject);
|
||||
|
||||
// Add them to the Protocol
|
||||
this.protocolMatchingRules.push(rule);
|
||||
});
|
||||
}
|
||||
|
||||
// If the input contains data for various Stages in the
|
||||
// display set sequence
|
||||
if (input.stages) {
|
||||
input.stages.forEach(stageObject => {
|
||||
// Create Stages from the stored data
|
||||
var stage = new HP.Stage();
|
||||
stage.fromObject(stageObject);
|
||||
|
||||
// Add them to the Protocol
|
||||
this.stages.push(stage);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a clone of the current Protocol with a new name
|
||||
*
|
||||
* Note! This method absolutely cannot be renamed 'clone', because
|
||||
* Minimongo's insert method uses 'clone' internally and this
|
||||
* somehow causes very bizarre behaviour
|
||||
*
|
||||
* @param name
|
||||
* @returns {Protocol|*}
|
||||
*/
|
||||
createClone(name) {
|
||||
// Create a new JavaScript independent of the current Protocol
|
||||
var currentProtocol = $.extend({}, this);
|
||||
|
||||
// Create a new Protocol to return
|
||||
var clonedProtocol = new HP.Protocol();
|
||||
|
||||
// Apply the desired properties
|
||||
currentProtocol.id = clonedProtocol.id;
|
||||
clonedProtocol.fromObject(currentProtocol);
|
||||
|
||||
// If we have specified a name, assign it
|
||||
if (name) {
|
||||
clonedProtocol.name = name;
|
||||
}
|
||||
|
||||
// Remove any MongoDB ID the current protocol may have had
|
||||
delete clonedProtocol._id;
|
||||
|
||||
// Unlock the clone
|
||||
clonedProtocol.locked = false;
|
||||
|
||||
// Return the cloned Protocol
|
||||
return clonedProtocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Role to the Set of Roles that have access
|
||||
* to this Protocol
|
||||
*
|
||||
* @param role
|
||||
*/
|
||||
addAvailableTo(role) {
|
||||
// Add the role's MongoDB _id to the availableTo Set
|
||||
this.availableTo.add(role._id);
|
||||
|
||||
// Update the modifiedDate and User that last
|
||||
// modified this Protocol
|
||||
this.protocolWasModified();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Role to the Set of Roles that have access
|
||||
* to this Protocol
|
||||
*
|
||||
* @param role
|
||||
*/
|
||||
addEditableBy(role) {
|
||||
// Add the role's MongoDB _id to the editableBy Set
|
||||
this.editableBy.add(role._id);
|
||||
|
||||
// Update the modifiedDate and User that last
|
||||
// modified this Protocol
|
||||
this.protocolWasModified();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a Stage to this Protocol's display set sequence
|
||||
*
|
||||
* @param stage
|
||||
*/
|
||||
addStage(stage) {
|
||||
this.stages.push(stage);
|
||||
|
||||
// Update the modifiedDate and User that last
|
||||
// modified this Protocol
|
||||
this.protocolWasModified();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a Rule to this Protocol's array of matching rules
|
||||
*
|
||||
* @param rule
|
||||
*/
|
||||
addProtocolMatchingRule(rule) {
|
||||
this.protocolMatchingRules.push(rule);
|
||||
|
||||
// Update the modifiedDate and User that last
|
||||
// modified this Protocol
|
||||
this.protocolWasModified();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a Rule from this Protocol's array of matching rules
|
||||
*
|
||||
* @param rule
|
||||
*/
|
||||
removeProtocolMatchingRule(rule) {
|
||||
var wasRemoved = removeFromArray(this.protocolMatchingRules, rule);
|
||||
|
||||
// Update the modifiedDate and User that last
|
||||
// modified this Protocol
|
||||
if (wasRemoved) {
|
||||
this.protocolWasModified();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This Class represents a Rule to be evaluated given a set of attributes
|
||||
* Rules have:
|
||||
* - An attribute (e.g. 'seriesDescription')
|
||||
* - A constraint Object, in the form required by Validate.js:
|
||||
*
|
||||
* rule.constraint = {
|
||||
* contains: {
|
||||
* value: 'T-1'
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* Note: In this example we use the 'contains' Validator, which is a custom Validator defined in Viewerbase
|
||||
*
|
||||
* - A value for whether or not they are Required to be matched (default: False)
|
||||
* - A value for their relative weighting during Protocol or Image matching (default: 1)
|
||||
*/
|
||||
class Rule {
|
||||
/**
|
||||
* The Constructor for the Class to create a Rule with the bare
|
||||
* minimum information
|
||||
*
|
||||
* @param name The desired name for the Rule
|
||||
*/
|
||||
constructor(attribute, constraint, required, weight) {
|
||||
// Create a new UUID for this Rule
|
||||
this.id = uuid.new();
|
||||
|
||||
// Set the Rule's weight (defaults to 1)
|
||||
this.weight = weight || 1;
|
||||
|
||||
// If an attribute is specified, assign it
|
||||
if (attribute) {
|
||||
this.attribute = attribute;
|
||||
}
|
||||
|
||||
// If a constraint is specified, assign it
|
||||
if (constraint) {
|
||||
this.constraint = constraint;
|
||||
}
|
||||
|
||||
// If a value for 'required' is specified, assign it
|
||||
if (required === undefined) {
|
||||
// If no value was specified, default to False
|
||||
this.required = false;
|
||||
} else {
|
||||
this.required = required;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Occasionally the Rule class needs to be instantiated from a JavaScript Object.
|
||||
* This function fills in a Protocol with the Object data.
|
||||
*
|
||||
* @param input A Rule as a JavaScript Object, e.g. retrieved from MongoDB or JSON
|
||||
*/
|
||||
fromObject(input) {
|
||||
// Check if the input already has an ID
|
||||
// If so, keep it. It not, create a new UUID
|
||||
this.id = input.id || uuid.new();
|
||||
|
||||
// Assign the specified input data to the Rule
|
||||
this.required = input.required;
|
||||
this.weight = input.weight;
|
||||
this.attribute = input.attribute;
|
||||
this.constraint = input.constraint;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The ProtocolMatchingRule Class extends the Rule Class.
|
||||
*
|
||||
* At present it does not add any new methods or attributes
|
||||
* @type {ProtocolMatchingRule}
|
||||
*/
|
||||
HP.ProtocolMatchingRule = class ProtocolMatchingRule extends Rule {};
|
||||
|
||||
/**
|
||||
* A Stage is one step in the Display Set Sequence for a Hanging Protocol
|
||||
*
|
||||
* Stages are defined as a ViewportStructure and an array of Viewports
|
||||
*
|
||||
* @type {Stage}
|
||||
*/
|
||||
HP.Stage = class Stage {
|
||||
constructor(ViewportStructure, name) {
|
||||
// Create a new UUID for this Stage
|
||||
this.id = uuid.new();
|
||||
|
||||
// Assign the name and ViewportStructure provided
|
||||
this.name = name;
|
||||
this.viewportStructure = ViewportStructure;
|
||||
|
||||
// Create an empty array for the Viewports
|
||||
this.viewports = [];
|
||||
|
||||
// Set the created date to Now
|
||||
this.createdDate = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a clone of the current Stage with a new name
|
||||
*
|
||||
* Note! This method absolutely cannot be renamed 'clone', because
|
||||
* Minimongo's insert method uses 'clone' internally and this
|
||||
* somehow causes very bizarre behaviour
|
||||
*
|
||||
* @param name
|
||||
* @returns {Stage|*}
|
||||
*/
|
||||
createClone(name) {
|
||||
// Create a new JavaScript independent of the current Protocol
|
||||
var currentStage = $.extend({}, this);
|
||||
|
||||
// Create a new Stage to return
|
||||
var clonedStage = new HP.Stage();
|
||||
|
||||
// Assign the desired properties
|
||||
currentStage.id = clonedStage.id;
|
||||
clonedStage.fromObject(currentStage);
|
||||
|
||||
// If we have specified a name, assign it
|
||||
if (name) {
|
||||
clonedStage.name = name;
|
||||
}
|
||||
|
||||
// Return the cloned Stage
|
||||
return clonedStage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Occasionally the Stage class needs to be instantiated from a JavaScript Object.
|
||||
* This function fills in a Protocol with the Object data.
|
||||
*
|
||||
* @param input A Stage as a JavaScript Object, e.g. retrieved from MongoDB or JSON
|
||||
*/
|
||||
fromObject(input) {
|
||||
// Check if the input already has an ID
|
||||
// If so, keep it. It not, create a new UUID
|
||||
this.id = input.id || uuid.new();
|
||||
|
||||
// Assign the input name to the Stage
|
||||
this.name = input.name;
|
||||
|
||||
// If a ViewportStructure is present in the input, add it from the
|
||||
// input data
|
||||
this.viewportStructure = new HP.ViewportStructure();
|
||||
this.viewportStructure.fromObject(input.viewportStructure);
|
||||
|
||||
// If any viewports are present in the input object
|
||||
if (input.viewports) {
|
||||
input.viewports.forEach(viewportObject => {
|
||||
// Create a new Viewport with their data
|
||||
var viewport = new HP.Viewport();
|
||||
viewport.fromObject(viewportObject);
|
||||
|
||||
// Add it to the viewports array
|
||||
this.viewports.push(viewport);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The ViewportStructure class represents the layout and layout properties that
|
||||
* Viewports are displayed in. ViewportStructure has a type, which corresponds to
|
||||
* a layout template, and a set of properties, which depend on the type.
|
||||
*
|
||||
* @type {ViewportStructure}
|
||||
*/
|
||||
HP.ViewportStructure = class ViewportStructure {
|
||||
constructor(type, properties) {
|
||||
this.type = type;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Occasionally the ViewportStructure class needs to be instantiated from a JavaScript Object.
|
||||
* This function fills in a ViewportStructure with the Object data.
|
||||
*
|
||||
* @param input The ViewportStructure as a JavaScript Object, e.g. retrieved from MongoDB or JSON
|
||||
*/
|
||||
fromObject(input) {
|
||||
this.type = input.type;
|
||||
this.properties = input.properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the layout template name based on the layout type
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
getLayoutTemplateName() {
|
||||
// Viewport structure can be updated later when we build more complex display layouts
|
||||
switch (this.type) {
|
||||
case 'grid':
|
||||
return 'gridLayout';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the number of Viewports required for this layout
|
||||
* given the layout type and properties
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
getNumViewports() {
|
||||
// Viewport structure can be updated later when we build more complex display layouts
|
||||
switch (this.type) {
|
||||
case 'grid':
|
||||
// For the typical grid layout, we only need to multiply rows by columns to
|
||||
// obtain the number of viewports
|
||||
return this.properties.rows * this.properties.columns;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This Class defines a Viewport in the Hanging Protocol Stage. A Viewport contains
|
||||
* arrays of Rules that are matched in the ProtocolEngine in order to determine which
|
||||
* images should be hung.
|
||||
*
|
||||
* @type {Viewport}
|
||||
*/
|
||||
HP.Viewport = class Viewport {
|
||||
constructor() {
|
||||
this.viewportSettings = {};
|
||||
this.imageMatchingRules = [];
|
||||
this.seriesMatchingRules = [];
|
||||
this.studyMatchingRules = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Occasionally the Viewport class needs to be instantiated from a JavaScript Object.
|
||||
* This function fills in a Viewport with the Object data.
|
||||
*
|
||||
* @param input The Viewport as a JavaScript Object, e.g. retrieved from MongoDB or JSON
|
||||
*/
|
||||
fromObject(input) {
|
||||
// If ImageMatchingRules exist, create them from the Object data
|
||||
// and add them to the Viewport's imageMatchingRules array
|
||||
if (input.imageMatchingRules) {
|
||||
input.imageMatchingRules.forEach(ruleObject => {
|
||||
var rule = new HP.ImageMatchingRule();
|
||||
rule.fromObject(ruleObject);
|
||||
this.imageMatchingRules.push(rule);
|
||||
});
|
||||
}
|
||||
|
||||
// If SeriesMatchingRules exist, create them from the Object data
|
||||
// and add them to the Viewport's seriesMatchingRules array
|
||||
if (input.seriesMatchingRules) {
|
||||
input.seriesMatchingRules.forEach(ruleObject => {
|
||||
var rule = new HP.SeriesMatchingRule();
|
||||
rule.fromObject(ruleObject);
|
||||
this.seriesMatchingRules.push(rule);
|
||||
});
|
||||
}
|
||||
|
||||
// If StudyMatchingRules exist, create them from the Object data
|
||||
// and add them to the Viewport's studyMatchingRules array
|
||||
if (input.studyMatchingRules) {
|
||||
input.studyMatchingRules.forEach(ruleObject => {
|
||||
var rule = new HP.StudyMatchingRule();
|
||||
rule.fromObject(ruleObject);
|
||||
this.studyMatchingRules.push(rule);
|
||||
});
|
||||
}
|
||||
|
||||
// If ViewportSettings exist, add them to the current protocol
|
||||
if (input.viewportSettings) {
|
||||
this.viewportSettings = input.viewportSettings;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and removes a rule from whichever array it exists in.
|
||||
* It is not required to specify if it exists in studyMatchingRules,
|
||||
* seriesMatchingRules, or imageMatchingRules
|
||||
*
|
||||
* @param rule
|
||||
*/
|
||||
removeRule(rule) {
|
||||
var array;
|
||||
if (rule instanceof HP.StudyMatchingRule) {
|
||||
array = this.studyMatchingRules;
|
||||
} else if (rule instanceof HP.SeriesMatchingRule) {
|
||||
array = this.seriesMatchingRules;
|
||||
} else if (rule instanceof HP.ImageMatchingRule) {
|
||||
array = this.imageMatchingRules;
|
||||
}
|
||||
|
||||
removeFromArray(array, rule);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The ImageMatchingRule class extends the Rule Class.
|
||||
*
|
||||
* At present it does not add any new methods or attributes
|
||||
* @type {ImageMatchingRule}
|
||||
*/
|
||||
HP.ImageMatchingRule = class ImageMatchingRule extends Rule {};
|
||||
|
||||
/**
|
||||
* The SeriesMatchingRule Class extends the Rule Class.
|
||||
*
|
||||
* At present it does not add any new methods or attributes
|
||||
* @type {SeriesMatchingRule}
|
||||
*/
|
||||
HP.SeriesMatchingRule = class SeriesMatchingRule extends Rule {};
|
||||
|
||||
/**
|
||||
* The StudyMatchingRule Class extends the Rule Class.
|
||||
*
|
||||
* At present it does not add any new methods or attributes
|
||||
* @type {StudyMatchingRule}
|
||||
*/
|
||||
HP.StudyMatchingRule = class StudyMatchingRule extends Rule {};
|
||||
@@ -0,0 +1,109 @@
|
||||
function getDefaultProtocol() {
|
||||
var protocol = new HP.Protocol('Default');
|
||||
protocol.id = 'defaultProtocol';
|
||||
protocol.locked = true;
|
||||
|
||||
var oneByOne = new HP.ViewportStructure('grid', {
|
||||
rows: 1,
|
||||
columns: 1
|
||||
});
|
||||
|
||||
var viewport = new HP.Viewport();
|
||||
var first = new HP.Stage(oneByOne, 'oneByOne');
|
||||
first.viewports.push(viewport);
|
||||
|
||||
protocol.stages.push(first);
|
||||
|
||||
HP.defaultProtocol = protocol;
|
||||
return HP.defaultProtocol;
|
||||
}
|
||||
|
||||
function getMRTwoByTwoTest() {
|
||||
var proto = new HP.Protocol('MR_TwoByTwo');
|
||||
proto.locked = true;
|
||||
// Use http://localhost:3000/viewer/1.2.840.113619.2.5.1762583153.215519.978957063.78
|
||||
|
||||
var studyInstanceUid = new HP.ProtocolMatchingRule('studyInstanceUid', {
|
||||
equals: {
|
||||
value: '1.2.840.113619.2.5.1762583153.215519.978957063.78'
|
||||
}
|
||||
}, true);
|
||||
|
||||
proto.addProtocolMatchingRule(studyInstanceUid);
|
||||
|
||||
var oneByTwo = new HP.ViewportStructure('grid', {
|
||||
rows: 1,
|
||||
columns: 2
|
||||
});
|
||||
|
||||
// Stage 1
|
||||
var left = new HP.Viewport();
|
||||
var right = new HP.Viewport();
|
||||
|
||||
var firstSeries = new HP.SeriesMatchingRule('seriesNumber', {
|
||||
equals: {
|
||||
value: 1
|
||||
}
|
||||
});
|
||||
|
||||
var secondSeries = new HP.SeriesMatchingRule('seriesNumber', {
|
||||
equals: {
|
||||
value: 2
|
||||
}
|
||||
});
|
||||
|
||||
var thirdImage = new HP.ImageMatchingRule('instanceNumber', {
|
||||
equals: {
|
||||
value: 3
|
||||
}
|
||||
});
|
||||
|
||||
left.seriesMatchingRules.push(firstSeries);
|
||||
left.imageMatchingRules.push(thirdImage);
|
||||
|
||||
right.seriesMatchingRules.push(secondSeries);
|
||||
right.imageMatchingRules.push(thirdImage);
|
||||
|
||||
var first = new HP.Stage(oneByTwo, 'oneByTwo');
|
||||
first.viewports.push(left);
|
||||
first.viewports.push(right);
|
||||
|
||||
proto.stages.push(first);
|
||||
|
||||
// Stage 2
|
||||
var twoByOne = new HP.ViewportStructure('grid', {
|
||||
rows: 2,
|
||||
columns: 1
|
||||
});
|
||||
var left2 = new HP.Viewport();
|
||||
var right2 = new HP.Viewport();
|
||||
|
||||
var fourthSeries = new HP.SeriesMatchingRule('seriesNumber', {
|
||||
equals: {
|
||||
value: 4
|
||||
}
|
||||
});
|
||||
|
||||
var fifthSeries = new HP.SeriesMatchingRule('seriesNumber', {
|
||||
equals: {
|
||||
value: 5
|
||||
}
|
||||
});
|
||||
|
||||
left2.seriesMatchingRules.push(fourthSeries);
|
||||
left2.imageMatchingRules.push(thirdImage);
|
||||
right2.seriesMatchingRules.push(fifthSeries);
|
||||
right2.imageMatchingRules.push(thirdImage);
|
||||
|
||||
var second = new HP.Stage(twoByOne, 'twoByOne');
|
||||
second.viewports.push(left2);
|
||||
second.viewports.push(right2);
|
||||
|
||||
proto.stages.push(second);
|
||||
|
||||
HP.testProtocol = proto;
|
||||
return HP.testProtocol;
|
||||
}
|
||||
|
||||
getDefaultProtocol();
|
||||
getMRTwoByTwoTest();
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
// Create package logger using loglevel
|
||||
// https://atmospherejs.com/spacejamio/loglevel
|
||||
log = loglevel.createPackageLogger('hangingprotocols', defaultLevel = 'info');
|
||||
@@ -5,13 +5,91 @@ Package.describe({
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
api.use('cornerstone');;
|
||||
api.versionsFrom('1.2.1');
|
||||
|
||||
api.addFiles('server/namespace.js', 'server');
|
||||
api.addFiles('server/dataDictionary.js', 'server');
|
||||
api.addFiles('server/instanceDataToJsObject.js', 'server');
|
||||
api.use('standard-app-packages');
|
||||
api.use('ecmascript');
|
||||
api.use('jquery');
|
||||
api.use('stylus');
|
||||
api.use('rwatts:uuid');
|
||||
api.use('practicalmeteor:loglevel');
|
||||
api.use('templating');
|
||||
api.use('natestrauser:select2@4.0.1', 'client');
|
||||
api.use('clinical:router');
|
||||
|
||||
api.export('instanceDataToJsObject', 'server');
|
||||
api.export('TAG_DICT', 'server');
|
||||
api.export('DICOMHP', [ 'client', 'server' ]);
|
||||
// Our custom packages
|
||||
api.use('viewerbase');
|
||||
|
||||
// This sets the default logging level of the package using the
|
||||
// loglevel package. It can be overridden in the JavaScript
|
||||
// console for debugging purposes
|
||||
api.addFiles('log.js');
|
||||
|
||||
api.addAssets('assets/dots.svg', 'client');
|
||||
|
||||
// Both client & server
|
||||
api.addFiles('both/namespace.js');
|
||||
api.addFiles('both/collections.js');
|
||||
//api.addFiles('both/dicomTagDescriptions.js');
|
||||
api.addFiles('both/schema.js');
|
||||
api.addFiles('both/routes.js');
|
||||
api.addFiles('both/hardcodedData.js');
|
||||
api.addFiles('both/testData.js');
|
||||
|
||||
// Client-only
|
||||
api.addFiles('client/collections.js', 'client');
|
||||
api.addFiles('client/protocolEngine.js', 'client');
|
||||
api.addFiles('client/helpers/displayConstraint.js', 'client');
|
||||
api.addFiles('client/helpers/attributes.js', 'client');
|
||||
|
||||
// UI Components
|
||||
api.addFiles('client/components/hangingProtocolButtons/hangingProtocolButtons.html', 'client');
|
||||
api.addFiles('client/components/hangingProtocolButtons/hangingProtocolButtons.js', 'client');
|
||||
|
||||
api.addFiles('client/components/matchedProtocols/matchedProtocols.html', 'client');
|
||||
api.addFiles('client/components/matchedProtocols/matchedProtocols.styl', 'client');
|
||||
api.addFiles('client/components/matchedProtocols/matchedProtocols.js', 'client');
|
||||
|
||||
api.addFiles('client/components/protocolEditor/protocolEditor.html', 'client');
|
||||
api.addFiles('client/components/protocolEditor/protocolEditor.styl', 'client');
|
||||
api.addFiles('client/components/protocolEditor/protocolEditor.js', 'client');
|
||||
|
||||
api.addFiles('client/components/ruleTable/ruleTable.html', 'client');
|
||||
api.addFiles('client/components/ruleTable/ruleTable.styl', 'client');
|
||||
api.addFiles('client/components/ruleTable/ruleTable.js', 'client');
|
||||
|
||||
api.addFiles('client/components/ruleEntryDialog/ruleEntryDialog.html', 'client');
|
||||
api.addFiles('client/components/ruleEntryDialog/ruleEntryDialog.styl', 'client');
|
||||
api.addFiles('client/components/ruleEntryDialog/ruleEntryDialog.js', 'client');
|
||||
|
||||
api.addFiles('client/components/settingEntryDialog/settingEntryDialog.html', 'client');
|
||||
api.addFiles('client/components/settingEntryDialog/settingEntryDialog.styl', 'client');
|
||||
api.addFiles('client/components/settingEntryDialog/settingEntryDialog.js', 'client');
|
||||
|
||||
api.addFiles('client/components/textEntryDialog/textEntryDialog.html', 'client');
|
||||
api.addFiles('client/components/textEntryDialog/textEntryDialog.styl', 'client');
|
||||
api.addFiles('client/components/textEntryDialog/textEntryDialog.js', 'client');
|
||||
|
||||
api.addFiles('client/components/settingsTable/settingsTable.html', 'client');
|
||||
api.addFiles('client/components/settingsTable/settingsTable.styl', 'client');
|
||||
api.addFiles('client/components/settingsTable/settingsTable.js', 'client');
|
||||
|
||||
api.addFiles('client/components/stageDetails/stageDetails.html', 'client');
|
||||
api.addFiles('client/components/stageDetails/stageDetails.styl', 'client');
|
||||
api.addFiles('client/components/stageDetails/stageDetails.js', 'client');
|
||||
|
||||
api.addFiles('client/components/stageSortable/stageSortable.html', 'client');
|
||||
api.addFiles('client/components/stageSortable/stageSortable.styl', 'client');
|
||||
api.addFiles('client/components/stageSortable/stageSortable.js', 'client');
|
||||
|
||||
// Server-only
|
||||
api.addFiles('server/collections.js', 'server');
|
||||
api.addFiles('server/methods.js', 'server');
|
||||
|
||||
// Global exports
|
||||
api.export('HP');
|
||||
|
||||
// Collections
|
||||
api.export('HangingProtocols');
|
||||
api.export('MatchedProtocols');
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
Meteor.publish('hangingprotocols', function() {
|
||||
// TODO: filter by availableTo user
|
||||
return HangingProtocols.find();
|
||||
});
|
||||
|
||||
Meteor.startup(function() {
|
||||
// Uncomment this next line to reset all your Protocols on every server reset
|
||||
// HangingProtocols.remove({});
|
||||
|
||||
if (HangingProtocols.find().count() === 0) {
|
||||
console.log('Inserting default protocols');
|
||||
HangingProtocols.insert(HP.defaultProtocol);
|
||||
HangingProtocols.insert(HP.testProtocol);
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,78 +0,0 @@
|
||||
/**
|
||||
* converts an implicit dataSet to a javascript object with the help of a data dictionary
|
||||
*
|
||||
* @param dataSet
|
||||
* @param options
|
||||
*/
|
||||
dataSetToJsWithDictionary = function(dataSet, dictionary, options) {
|
||||
if (!dataSet) {
|
||||
throw 'dataSetToJsWithDictionary: missing required parameter dataSet';
|
||||
}
|
||||
|
||||
options = options || {
|
||||
omitPrivateAttibutes: true, // true if private elements should be omitted
|
||||
maxElementLength: 128 // maximum element length to try and convert to string format
|
||||
};
|
||||
|
||||
// For the purpose of inserting a comma into the tag in order
|
||||
// to search the dictionary, store a position value
|
||||
var position = 3;
|
||||
|
||||
var result = {};
|
||||
|
||||
for (var tag in dataSet.elements) {
|
||||
var element = dataSet.elements[tag];
|
||||
|
||||
// Reformat the tag from x00020010 to "0002,0010" to suit the dictionary
|
||||
var tagFormatted = tagFormatted.substr(1, position) + ',' + tagFormatted.substr(position);
|
||||
|
||||
// Retrieve the tag dictionary entry
|
||||
var tagDictionaryEntry = dictionary[tagFormatted];
|
||||
|
||||
// If there is no tag dictionary entry, or no name
|
||||
// for this tag, just use the original tag instead
|
||||
var tagName;
|
||||
if (!tagDictionaryEntry || !tagDictionaryEntry.name) {
|
||||
tagName = tag;
|
||||
} else {
|
||||
tagName = tagDictionaryEntry.name;
|
||||
}
|
||||
|
||||
// Set the element VR from the dictionary, if necessary and possible
|
||||
if (!element.vr && tagDictionaryEntry && tagDictionaryEntry.vr) {
|
||||
element.vr = tagDictionaryEntry.vr;
|
||||
}
|
||||
|
||||
// skip this element if it a private element and our options specify that we should
|
||||
if (options.omitPrivateAttibutes === true && dicomParser.isPrivateTag(tag)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (element.items) {
|
||||
// handle sequences
|
||||
var sequenceItems = [];
|
||||
for (var i = 0; i < element.items.length; i++) {
|
||||
sequenceItems.push(dicomParser.dataSetToJsWithDictionary(element.items[i].dataSet, dictionary, options));
|
||||
}
|
||||
|
||||
result[tagName] = sequenceItems;
|
||||
} else {
|
||||
var asString;
|
||||
|
||||
if (element.length < options.maxElementLength) {
|
||||
asString = dicomParser.explicitElementToString(dataSet, element);
|
||||
}
|
||||
|
||||
if (asString !== undefined) {
|
||||
result[tagName] = asString;
|
||||
} else {
|
||||
result[tagName] = {
|
||||
dataOffset: element.dataOffset,
|
||||
length: element.length
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
DICOMHP.imageSet = function(setNumber, category) {
|
||||
this.setNumber = setNumber;
|
||||
this.category = category;
|
||||
};
|
||||
|
||||
DICOMHP.imageSet.prototype.setRelativeTime = function(time) {
|
||||
this.relativeTime = time;
|
||||
};
|
||||
|
||||
DICOMHP.imageSet.prototype.setTimeUnits = function(units) {
|
||||
this.timeUnits = units;
|
||||
};
|
||||
|
||||
DICOMHP.imageSet.prototype.setPriorValue = function(priorValue) {
|
||||
this.priorValue = priorValue;
|
||||
};
|
||||
|
||||
DICOMHP.imageSet.prototype.retrieve = function(studyInstanceId) {
|
||||
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
var valueRepresentationTypes = {
|
||||
number: [
|
||||
'SH',
|
||||
'LO'
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* converts an WADO instance to a javascript object with the help of a data dictionary
|
||||
*
|
||||
* @param instance
|
||||
* @param options
|
||||
*/
|
||||
instanceDataToJsObject = function(instance, dictionary) {
|
||||
if (!instance) {
|
||||
throw 'instanceDataToJsObject: missing required parameter dataSet';
|
||||
}
|
||||
|
||||
var result = {};
|
||||
|
||||
// The comma is always inserted after the first four digits
|
||||
var position = 4;
|
||||
|
||||
Object.keys(instance).forEach(function(tag) {
|
||||
var item = instance[tag];
|
||||
|
||||
// Reformat the tag from 00020010 to "(0002,0010)" to suit the dictionary
|
||||
var tagFormatted = '(' + tag.substr(0, position) + ',' + tag.substr(position) + ')';
|
||||
|
||||
// Retrieve the tag dictionary entry
|
||||
var tagDictionaryEntry = dictionary[tagFormatted];
|
||||
console.log(tagDictionaryEntry);
|
||||
|
||||
// If there is no tag dictionary entry, or no name
|
||||
// for this tag, just use the original tag instead
|
||||
var tagName;
|
||||
if (!tagDictionaryEntry || !tagDictionaryEntry.name) {
|
||||
tagName = tag;
|
||||
} else {
|
||||
tagName = tagDictionaryEntry.name;
|
||||
}
|
||||
|
||||
if (item.Value && item.Value.length > 1) {
|
||||
// handle sequences
|
||||
var sequenceItems = [];
|
||||
item.Value.forEach(function(instance) {
|
||||
sequenceItems.push(instanceDataToJsObject(instance, dictionary));
|
||||
});
|
||||
result[tagName] = sequenceItems;
|
||||
|
||||
} else if (!item.Value || !item.Value.length) {
|
||||
// Handle empty arrays
|
||||
result[tagName] = undefined;
|
||||
|
||||
} else {
|
||||
// Handle single valued cases
|
||||
if (item.vr === 'PN') {
|
||||
// Patient Name
|
||||
result[tagName] = DICOMWeb.getName(item);
|
||||
|
||||
} else if (valueRepresentationTypes.number.indexOf(item.vr) > 0) {
|
||||
// Number type
|
||||
result[tagName] = DICOMWeb.getNumber(item);
|
||||
|
||||
} else {
|
||||
// Everything else
|
||||
result[tagName] = DICOMWeb.getString(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log(result);
|
||||
return result;
|
||||
};
|
||||
@@ -1,87 +0,0 @@
|
||||
var tags = {
|
||||
sopClassUid: '00080016'
|
||||
};
|
||||
|
||||
var tagValues = {
|
||||
HangingProtocolStorage: '1.2.840.10008.5.1.4.38.1'
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds appropriate hanging protocols given a studyInstanceUid
|
||||
*
|
||||
* This function retrieves all hanging protocols in storage
|
||||
* in order to find those that match the parameters of the given study
|
||||
*
|
||||
* @param studyInstanceUID
|
||||
* @returns {Array} An array of matching hanging protocols
|
||||
*/
|
||||
DICOMHP.match = function(studyInstanceUID) {
|
||||
// First, retrieve the metaData from the given study
|
||||
var studyInstance = Services.WADO.retrieveMetadata(studyInstanceUID);
|
||||
if (!studyInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: It turns out Orthanc doesn't support hanging protocol storage yet,
|
||||
// so I will just stop here.
|
||||
var studyData = instanceDataToJsObject(studyInstance, TAG_DICT);
|
||||
|
||||
/*var searchParameters = {
|
||||
tags['sopClassUid']: tagValues['HangingProtocolStorage']
|
||||
};
|
||||
|
||||
|
||||
var modalities = studyData.ModalitiesInStudy;
|
||||
|
||||
|
||||
var modalityMatch;
|
||||
|
||||
if (modalities) {
|
||||
modalityMatch = [];
|
||||
|
||||
modalities.split('\\').forEach(function(modality){
|
||||
if (modality && modality != 'SC') {
|
||||
modalityMatch.push(modality);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
var firstInstance = Services.WADO..retrieveMetadata(studyInstanceUID, null, null, {limit : 1}),
|
||||
lateralityMatch;
|
||||
|
||||
if (firstInstance.length > 0) {
|
||||
lateralityMatch = firstInstance[0]["00200060"].Value[0];
|
||||
}
|
||||
|
||||
// since orthanc doesn't support sequence matching
|
||||
var allProtocols = Services.WADO..searchForInstances(null, null, params);
|
||||
|
||||
if (allProtocols.length < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var matchedProtocols = [];
|
||||
|
||||
allProtocols.forEach(function(protocol) {
|
||||
|
||||
var definitionSequence = protocol["0072000C"].Value;
|
||||
|
||||
definitionSequence.forEach(function(definition){
|
||||
|
||||
var defModality = definition["00080060"].Value[0];
|
||||
|
||||
if (defModality && modalityMatch && modalityMatch.indexOf(defModality) != -1) {
|
||||
matchedProtocols.push(protocol);
|
||||
return;
|
||||
}
|
||||
|
||||
var defLaterality = definition["00200060"].Value[0];
|
||||
if (defLaterality && (defLaterality == lateralityMatch)) {
|
||||
matchedProtocols.push(protocol);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return matchedProtocols;*/
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
/*DICOMTags = new Meteor.Collection(null);
|
||||
|
||||
Object.keys(HP.tagDescriptions).forEach(function(key) {
|
||||
var value = HP.tagDescriptions[key];
|
||||
DICOMTags.insert({
|
||||
id: key,
|
||||
name: '(' + key + ') ' + value
|
||||
});
|
||||
});
|
||||
|
||||
Meteor.methods({
|
||||
dicomTagSearch: function(partialName) {
|
||||
check(partialName, String);
|
||||
|
||||
var results = DICOMTags.find({
|
||||
name: {
|
||||
$regex: partialName,
|
||||
$options: 'i'
|
||||
}
|
||||
}, {
|
||||
limit: 20,
|
||||
fields: {
|
||||
id: 1,
|
||||
name: 1
|
||||
}
|
||||
}).fetch();
|
||||
|
||||
return {
|
||||
results: results
|
||||
};
|
||||
}
|
||||
}); */
|
||||
|
||||
Meteor.methods({
|
||||
removeHangingProtocol: function(id) {
|
||||
HangingProtocols.remove(id);
|
||||
},
|
||||
removeHangingProtocolByID: function(id) {
|
||||
HangingProtocols.remove({
|
||||
id: id
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
DICOMHP = {};
|
||||
@@ -1,78 +0,0 @@
|
||||
DICOMHP.select = function(hpInstance) {
|
||||
var imageSetsSequence = hpInstance['00720020'].Value;
|
||||
if (!imageSetsSequence) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var matchedImageSets = [];
|
||||
imageSetsSequence.forEach(function(imageSet) {
|
||||
var selectorSequence = imageSet['00720022'].Value;
|
||||
selectorSequence.forEach(function(selector) {
|
||||
var usageFlag = selector['00720024'].Value[0],
|
||||
selectorAttribute = selector['00720026'].Value[0],
|
||||
selectorAttributeVR = selector['00720050'].Value[0],
|
||||
selectorAttributeValue = null;
|
||||
|
||||
if (selectorAttributeVR == 'SQ') {
|
||||
return;
|
||||
} else if (selectorAttributeVR == 'AT') {
|
||||
selectorAttributeValue = selector['00720060'].Value[0];
|
||||
} else if (selectorAttributeVR == 'CS') {
|
||||
selectorAttributeValue = selector['00720062'].Value[0];
|
||||
} else if (selectorAttributeVR == 'IS') {
|
||||
selectorAttributeValue = selector['00720064'].Value[0];
|
||||
} else if (selectorAttributeVR == 'LO') {
|
||||
selectorAttributeValue = selector['00720066'].Value[0];
|
||||
} else if (selectorAttributeVR == 'LT') {
|
||||
selectorAttributeValue = selector['00720068'].Value[0];
|
||||
} else if (selectorAttributeVR == 'PN') {
|
||||
selectorAttributeValue = selector['0072006A'].Value[0];
|
||||
} else if (selectorAttributeVR == 'SH') {
|
||||
selectorAttributeValue = selector['0072006C'].Value[0];
|
||||
} else if (selectorAttributeVR == 'ST') {
|
||||
selectorAttributeValue = selector['0072006E'].Value[0];
|
||||
} else if (selectorAttributeVR == 'UT') {
|
||||
selectorAttributeValue = selector['00720070'].Value[0];
|
||||
} else if (selectorAttributeVR == 'DS') {
|
||||
selectorAttributeValue = selector['00720072'].Value[0];
|
||||
} else if (selectorAttributeVR == 'FD') {
|
||||
selectorAttributeValue = selector['00720074'].Value[0];
|
||||
} else if (selectorAttributeVR == 'FL') {
|
||||
selectorAttributeValue = selector['00720076'].Value[0];
|
||||
} else if (selectorAttributeVR == 'UL') {
|
||||
selectorAttributeValue = selector['00720078'].Value[0];
|
||||
} else if (selectorAttributeVR == 'US') {
|
||||
selectorAttributeValue = selector['0072007A'].Value[0];
|
||||
} else if (selectorAttributeVR == 'SL') {
|
||||
selectorAttributeValue = selector['0072007C'].Value[0];
|
||||
} else if (selectorAttributeVR == 'SS') {
|
||||
selectorAttributeValue = selector['0072007E'].Value[0];
|
||||
}
|
||||
|
||||
if (selectorAttributeValue !== null) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
var timeBasedImageSetsSequence = imageSet['00720030'].Value;
|
||||
timeBasedImageSetsSequence.forEach(function(timeImageSet) {
|
||||
var setNumber = timeImageSet['00720032'].Value[0], selectorCategory = timeImageSet['00720034'].Value[0];
|
||||
|
||||
var mImageSet = new DICOMHP.imageSet(setNumber, selectorCategory);
|
||||
if (selectorCategory == 'RELATIVE_TIME') {
|
||||
var relativeTime = timeImageSet['00720038'].Value[0], timeUnits = timeImageSet['0072003A'].Value[0];
|
||||
|
||||
mImageSet.setRelativeTime(relativeTime);
|
||||
mImageSet.setTimeUnits(timeUnits);
|
||||
} else if (selectorCategory == 'ABSTRACT_PRIOR') {
|
||||
var priorValue = timeImageSet['0072003C'].Value[0];
|
||||
|
||||
mImageSet.setPriorValue(priorValue);
|
||||
}
|
||||
|
||||
matchedImageSets.push(mImageSet);
|
||||
});
|
||||
});
|
||||
|
||||
return matchedImageSets;
|
||||
};
|
||||
Reference in new issue
Block a user