First pass at a Hanging Protocol implementation and editor (LT-131)

This commit is contained in:
Erik Ziegler 2016-05-07 08:58:46 +02:00
parent dfcb33153f
commit 2e52d9e3c1
157 changed files with 8869 additions and 23585 deletions

View File

@ -43,7 +43,7 @@
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
"eqnull" : false, // true: Tolerate use of `== null`
"es5" : true, // true: Allow ES5 syntax (ex: getters and setters)
"esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`)
"esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`)
"moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
// (ex: `for each`, multiple try/catch, function expression…)
"evil" : false, // true: Tolerate use of `eval` and `new Function()`
@ -85,37 +85,5 @@
"typed" : false, // Globals for typed array constructions
"worker" : false, // Web Workers
"wsh" : false, // Windows Scripting Host
"yui" : false, // Yahoo User Interface
// Custom Globals
// additional predefined global variables
"globals" : {
console: true,
prompt: true,
$: true,
Hammer: true,
cornerstone: true,
cornerstoneMath: true,
cornerstoneTools: true,
cornerstoneTools: true,
// Meteor-specific Globals
Meteor: true,
Template: true,
UI: true,
Session: true,
// loglevel package
log: true,
// OHIF-package specific Globals
OHIF: true,
toolManager: true,
ViewerData: true,
ViewerStudies: true,
LesionManager: true,
Timepoints: true,
Measurements: true,
StudyImportStatus: true
}
"yui" : false // Yahoo User Interface
}

View File

@ -54,3 +54,4 @@ gilbertwat:bootstrap3-daterangepicker
email
peppelg:bootstrap-3-modal
simple:reactive-method
hangingprotocols

View File

@ -31,6 +31,7 @@ clinical:router-url@2.0.15
clinical:theming@0.4.10
coffeescript@1.0.11
cornerstone@0.0.1
dburles:mongo-collection-instances@0.3.4
ddp@1.2.2
ddp-client@1.2.1
ddp-common@1.2.2
@ -63,6 +64,7 @@ iron:core@1.0.11
iron:dynamic-template@1.0.12
iron:layout@1.0.12
jquery@1.11.4
lai:collection-extensions@0.1.4
launch-screen@1.0.4
lesiontracker@0.0.1
livedata@1.0.15
@ -79,10 +81,12 @@ momentjs:moment@2.12.0
mongo@1.1.3
mongo-id@1.0.1
mrt:moment@2.8.1
natestrauser:select2@4.0.2
npm-bcrypt@0.7.8_2
npm-mongo@1.4.39_1
observe-sequence@1.0.7
ordered-dict@1.0.4
orthanc-remote@0.0.1
peppelg:bootstrap-3-modal@1.0.4
practicalmeteor:chai@2.1.0_1
practicalmeteor:loglevel@1.2.0_2
@ -94,6 +98,7 @@ reactive-var@1.0.6
reload@1.1.4
retry@1.0.4
routepolicy@1.0.6
rubaxa:sortable@1.3.0
rwatts:uuid@0.0.2
service-configuration@1.0.5
session@1.1.1

View File

@ -1,6 +1,7 @@
<template name="viewerMain">
<div class="viewerMain">
{{ >toolbar toolbarOptions=toolbarOptions }}
{{ >imageViewerViewports }}
<div id='layoutManagerTarget'>
</div>
</div>
</template>

View File

@ -86,25 +86,23 @@ Template.viewerMain.helpers({
// CR/UN/EX Tools
var crunexToolsBtns = {
id: 'crunexTools',
tools:[{
tools: [{
id: 'crTool',
title: 'CR Tool',
classes: 'imageViewerTool',
iconClasses: 'fa fa-cr'
},
{
}, {
id: 'unTool',
title: 'UN Tool',
classes: 'imageViewerTool',
iconClasses: 'fa fa-un'
},
{
}, {
id: 'exTool',
title: 'EX Tool',
classes: 'imageViewerTool',
iconClasses: 'fa fa-ex'
}],
title: "CR/UN/EX",
title: 'CR/UN/EX',
groupIcon: 'fa fa-exchange'
};
@ -118,3 +116,12 @@ Template.viewerMain.helpers({
return toolbarOptions;
}
});
Template.viewerMain.onRendered(function() {
var parentNode = document.getElementById('layoutManagerTarget');
var studies = this.data.studies;
layoutManager = new LayoutManager(parentNode, studies);
ProtocolEngine = new HP.ProtocolEngine(layoutManager, studies);
HP.setEngine(ProtocolEngine);
});

View File

@ -2,15 +2,22 @@
width: 100%
height: 84%
#layoutManagerTarget
width: 100%
height: calc(100% - 30px)
//font awesome icons
.fa-cr:before
content: 'CR'
font-weight: bold
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif
.fa-un:before
content: 'UN'
font-weight: bold
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif
.fa-ex:before
content: 'EX'
font-weight: bold
font-weight: bold
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif

View File

@ -0,0 +1,183 @@
// Define Baseline protocol
var proto = new HP.Protocol('LT_Baseline');
proto.locked = true;
var studyDescription = new HP.ProtocolMatchingRule();
studyDescription.required = true;
studyDescription.attribute = 'studyDescription';
studyDescription.constraint = {
contains: {
value: 'CT'
}
};
var isBaseline = new HP.ProtocolMatchingRule();
isBaseline.required = true;
isBaseline.attribute = 'timepointType';
isBaseline.constraint = {
equals: {
value: 'baseline'
}
};
proto.addProtocolMatchingRule(studyDescription);
proto.addProtocolMatchingRule(isBaseline);
var oneByOne = new HP.ViewportStructure('grid', {
rows: 1,
columns: 1
});
// Stage 1
var single = new HP.Viewport();
var baseline = new HP.StudyMatchingRule(true);
baseline.required = true;
baseline.attribute = 'timepointType';
baseline.constraint = {
equals: {
value: 'baseline'
}
};
var body = new HP.SeriesMatchingRule();
body.attribute = 'seriesDescription';
body.weight = 5;
body.constraint = {
contains: {
value: 'Body'
}
};
var chest = new HP.SeriesMatchingRule();
chest.attribute = 'seriesDescription';
chest.constraint = {
contains: {
value: 'CHEST'
}
};
single.studyMatchingRules.push(baseline);
single.seriesMatchingRules.push(body);
single.seriesMatchingRules.push(chest);
var first = new HP.Stage(oneByOne, 'oneByOne');
first.viewports.push(single);
proto.addStage(first);
HP.lesionTrackerBaselineProtocol = proto;
HP.lesionTrackerBaselineProtocol.id = 'lesionTrackerBaselineProtocol';
// Define Followup Protocol
var proto = new HP.Protocol('LT_BaselineFollowup');
proto.locked = true;
var studyDescription = new HP.ProtocolMatchingRule();
studyDescription.required = true;
studyDescription.attribute = 'studyDescription';
studyDescription.constraint = {
contains: {
value: 'CT'
}
};
var isFollowup = new HP.ProtocolMatchingRule();
isFollowup.required = true;
isFollowup.attribute = 'timepointType';
isFollowup.constraint = {
equals: {
value: 'followup'
}
};
proto.addProtocolMatchingRule(studyDescription);
proto.addProtocolMatchingRule(isFollowup);
var oneByTwo = new HP.ViewportStructure('grid', {
rows: 1,
columns: 2
});
// Stage 1
var left = new HP.Viewport();
var right = new HP.Viewport();
var baseline = new HP.StudyMatchingRule(true);
baseline.required = true;
baseline.attribute = 'timepointType';
baseline.constraint = {
equals: {
value: 'baseline'
}
};
var followup = new HP.StudyMatchingRule();
followup.required = true;
followup.attribute = 'timepointType';
followup.constraint = {
equals: {
value: 'followup'
}
};
var body = new HP.SeriesMatchingRule();
body.attribute = 'seriesDescription';
body.weight = 5;
body.constraint = {
contains: {
value: 'Body'
}
};
var chest = new HP.SeriesMatchingRule();
chest.attribute = 'seriesDescription';
chest.constraint = {
contains: {
value: 'CHEST'
}
};
left.studyMatchingRules.push(baseline);
left.seriesMatchingRules.push(body);
left.seriesMatchingRules.push(chest);
right.studyMatchingRules.push(followup);
right.seriesMatchingRules.push(body);
right.seriesMatchingRules.push(chest);
var first = new HP.Stage(oneByTwo, 'oneByTwo');
first.viewports.push(left);
first.viewports.push(right);
proto.addStage(first);
HP.lesionTrackerFollowupProtocol = proto;
HP.lesionTrackerFollowupProtocol.id = 'lesionTrackerFollowupProtocol';
Meteor.call('removeHangingProtocolByID', HP.lesionTrackerBaselineProtocol.id, function() {
HangingProtocols.insert(HP.lesionTrackerBaselineProtocol);
});
Meteor.call('removeHangingProtocolByID', HP.lesionTrackerFollowupProtocol.id, function() {
HangingProtocols.insert(HP.lesionTrackerFollowupProtocol);
});
Meteor.startup(function() {
getTimepointType = function(study) {
var timepoint = Timepoints.findOne({
studyInstanceUids: {
$in: [study.studyInstanceUid]
}
});
if (!timepoint) {
return;
}
return timepoint.timepointType;
};
HP.addCustomAttribute('timepointType', 'Timepoint Type', getTimepointType);
});

View File

@ -15,8 +15,7 @@ Object.keys(ViewerData).forEach(function(contentId) {
Router.configure({
layoutTemplate: 'lesionTrackerLayout',
loadingTemplate: 'lesionTrackerLayout',
notFoundTemplate: 'notFound'
loadingTemplate: 'lesionTrackerLayout'
});
Router.onBeforeAction('loading');

View File

@ -4,9 +4,6 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
accounts-password
ian:accounts-ui-bootstrap-3
iron:router
twbs:bootstrap
http
arsnebula:reactive-promise
@ -37,3 +34,4 @@ practicalmeteor:loglevel
hangingprotocols
reactive-dict
gilbertwat:bootstrap3-daterangepicker
clinical:router

View File

@ -1,6 +1,3 @@
accounts-base@1.2.2
accounts-password@1.1.4
anti:i18n@0.4.3
arsnebula:reactive-promise@0.9.1
autoupdate@1.2.4
babel-compiler@5.8.24_1
@ -15,12 +12,15 @@ caching-compiler@1.0.0
caching-html-compiler@1.0.2
callback-hook@1.0.4
check@1.1.0
clinical:router@2.0.17
clinical:router-location@2.0.14
clinical:router-middleware-stack@2.0.13
clinical:router-url@2.0.15
coffeescript@1.0.11
cornerstone@0.0.1
ddp@1.2.2
ddp-client@1.2.1
ddp-common@1.2.2
ddp-rate-limiter@1.0.0
ddp-server@1.2.2
deps@1.0.9
dicomweb@0.0.1
@ -29,7 +29,6 @@ dimseservice@0.0.1
ecmascript@0.1.6
ecmascript-runtime@0.2.6
ejson@1.0.7
email@1.0.8
fastclick@1.0.7
fortawesome:fontawesome@4.4.0
geojson-utils@1.0.4
@ -39,20 +38,14 @@ hot-code-push@1.0.0
html-tools@1.0.5
htmljs@1.0.5
http@1.1.1
ian:accounts-ui-bootstrap-3@1.2.83
id-map@1.0.4
iron:controller@1.0.12
iron:core@1.0.11
iron:dynamic-template@1.0.12
iron:layout@1.0.12
iron:location@1.0.11
iron:middleware-stack@1.0.11
iron:router@1.0.12
iron:url@1.0.11
jquery@1.11.4
launch-screen@1.0.4
livedata@1.0.15
localstorage@1.0.5
logging@1.0.8
meteor@1.1.10
meteor-base@1.0.1
@ -65,28 +58,25 @@ momentjs:moment@2.9.0
mongo@1.1.3
mongo-id@1.0.1
mrt:moment@2.8.1
npm-bcrypt@0.7.8_2
natestrauser:select2@4.0.1
npm-mongo@1.4.39_1
observe-sequence@1.0.7
ordered-dict@1.0.4
orthanc-remote@0.0.1
practicalmeteor:chai@1.9.2_3
practicalmeteor:loglevel@1.1.0_3
promise@0.5.1
random@1.0.5
rate-limit@1.0.0
reactive-dict@1.1.3
reactive-var@1.0.6
reload@1.1.4
retry@1.0.4
routepolicy@1.0.6
rwatts:uuid@0.0.2
service-configuration@1.0.5
session@1.1.1
sha@1.0.4
silentcicero:jszip@0.0.4
spacebars@1.0.7
spacebars-compiler@1.0.7
srp@1.0.4
standard-app-packages@1.0.6
standard-minifiers@1.0.2
stylus@2.511.1

View File

@ -18,13 +18,6 @@
<li>
<a href="http://www.ohif.org">About</a>
</li>
<li>
<form class="navbar-form">
<div class="form-group">
{{> loginButtons align="right"}}
</div>
</form>
</li>
</ul>
</div>
</div>

View File

@ -9,11 +9,14 @@
.navbar
margin-bottom: 0
border-radius: 0
color: #888888
background-color: #060606
border: none
font-family: "Sanchez"
color: #C1C1C1
-webkit-transition: all 0.3s ease
-moz-transition: all 0.3s ease
transition: all 0.3s ease
.navbar-default .navbar-nav > li > a
color: #C1C1C1

View File

@ -1,13 +1,18 @@
<template name="viewer">
{{#if Template.subscriptionsReady}}
<div id="viewer">
{{> studyBrowser }}
<div class="studyBrowserSidebar">
{{ >relatedStudySelect }}
{{> studyBrowser }}
</div>
{{> cineDialog }}
<div class='viewerMain'>
{{> toolbar }}
{{> imageViewerViewports }}
<div id='layoutManagerTarget'>
</div>
</div>
</div>
{{ >protocolEditor }}
{{else}}
{{>loadingText}}
{{/if}}

View File

@ -1,6 +1,9 @@
Template.viewer.onCreated(function() {
// Attach the Window resize listener
$(window).on('resize', handleResize);
$('.navbar').width('100%');
Meteor.subscribe('hangingprotocols');
log.info('viewer onCreated');
@ -24,18 +27,22 @@ Template.viewer.onCreated(function() {
cornerstone.setViewport(element, viewport);
},
resetViewport: function(element) {
cornerstone.reset(element);
var enabledElement = cornerstone.getEnabledElement(element);
if (enabledElement.fitToWindow === false) {
var imageId = enabledElement.image.imageId;
var instance = cornerstoneTools.metaData.get('instance', imageId);
enabledElement.viewport = cornerstone.getDefaultViewport(enabledElement.canvas, enabledElement.image);
var instanceClassDefaultViewport = getInstanceClassDefaultViewport(instance, enabledElement, imageId);
cornerstone.setViewport(element, instanceClassDefaultViewport);
} else {
cornerstone.reset(element);
}
},
clearTools: function(element) {
var toolStateManager = cornerstoneTools.globalImageIdSpecificToolStateManager;
toolStateManager.clear(element);
cornerstone.updateImage(element);
},
previousPresentationGroup: function() {
WindowManager.previousPresentationGroup();
},
nextPresentationGroup: function() {
WindowManager.nextPresentationGroup();
}
};
@ -78,13 +85,18 @@ Template.viewer.onCreated(function() {
study.selected = true;
ViewerStudies.insert(study);
});
OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer('CornerstoneNewImage', cornerstoneTools.updateImageSynchronizer);
});
Template.viewer.onRendered(function() {
// Enable hotkeys
enableHotkeys();
var parentNode = document.getElementById('layoutManagerTarget');
var studies = this.data.studies;
layoutManager = new LayoutManager(parentNode, studies);
ProtocolEngine = new HP.ProtocolEngine(layoutManager, studies);
HP.setEngine(ProtocolEngine);
});
Template.viewer.onDestroyed(function() {
@ -92,6 +104,4 @@ Template.viewer.onDestroyed(function() {
// Remove the Window resize listener
$(window).off('resize', handleResize);
OHIF.viewer.updateImageSynchronizer.destroy();
});

View File

@ -18,4 +18,20 @@
float: left
background-color: gray
height: 100%
width: calc(100% - 120px)
width: calc(100% - 120px)
-webkit-transition: all 0.3s ease
-moz-transition: all 0.3s ease
transition: all 0.3s ease
#layoutManagerTarget
height: calc(100% - 30px)
width: 100%
.studyBrowserSidebar
float: left
height: 100%
width: 120px
overflow: hidden
background-color: #000
padding-top: 10px

View File

@ -1,6 +1,7 @@
Session.setDefault('ViewerData', {});
// Re-add any tab data saved in the Session
WorklistTabs.remove({});
Object.keys(ViewerData).forEach(function(contentId) {
var tabData = ViewerData[contentId];
var data = {
@ -12,8 +13,7 @@ Object.keys(ViewerData).forEach(function(contentId) {
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'layout',
notFoundTemplate: 'notFound'
loadingTemplate: 'layout'
});
Router.onBeforeAction('loading');
@ -22,6 +22,10 @@ Router.route('/', function() {
this.render('worklist');
});
Router.route('/worklist', function() {
this.render('worklist');
});
Router.route('/viewer/:_id', {
layoutTemplate: 'layout',
name: 'viewer',
@ -30,14 +34,19 @@ Router.route('/viewer/:_id', {
// Check if this study is already loaded in a tab
// If it is, stop here so we don't keep adding tabs on hot-code reloads
var tab = WorklistTabs.find({
var tabs = WorklistTabs.find({
studyInstanceUid: studyInstanceUid
}).fetch();
if (tab) {
});
if (tabs.count()) {
return;
}
this.render('worklist');
openNewTab(studyInstanceUid);
this.render('worklist', {
data: function() {
return {
studyInstanceUid: studyInstanceUid
};
}
});
}
});

View File

@ -36,6 +36,4 @@ Meteor.startup(function() {
}
//defaultServiceType: 'dimse'
};
console.log('Using default OHIF Viewer settings with service: ' + Meteor.settings.defaultServiceType);
});

View File

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

View File

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

View File

@ -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'
}];

View File

@ -0,0 +1 @@
HP = {};

View File

@ -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.');
}
});

View File

@ -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 {};

View File

@ -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();

View File

@ -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);
});

View File

@ -1,6 +1,6 @@
<template name="hangingProtocolButtons">
<button id="previousPresentationGroup" type="button"
class="imageViewerCommand btn btn-sm btn-default"
class="btn btn-sm btn-default"
data-container="body"
data-toggle="tooltip"
data-placement="bottom"
@ -9,7 +9,7 @@
<span class="fa fa-step-backward"></span>
</button>
<button id="nextPresentationGroup" type="button"
class="imageViewerCommand btn btn-sm btn-default"
class="btn btn-sm btn-default"
data-container="body"
data-toggle="tooltip"
data-placement="bottom"

View File

@ -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();
}
});

View File

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

View File

@ -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);
}
});

View File

@ -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;

View File

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

View File

@ -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);
}
});

View File

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

View File

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

View File

@ -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);
}
});

View File

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

View File

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

View File

@ -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);
}
});

View File

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

View File

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

View File

@ -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);
}
});

View File

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

View File

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

View File

@ -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);
}
}
});

View File

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

View File

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

View File

@ -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();
}
});

View File

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

View File

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

View File

@ -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());
}
});

View File

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

View File

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

View File

@ -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();
}
});

View File

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

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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);
}
};

View File

@ -0,0 +1,3 @@
// Create package logger using loglevel
// https://atmospherejs.com/spacejamio/loglevel
log = loglevel.createPackageLogger('hangingprotocols', defaultLevel = 'info');

View File

@ -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');
});

View File

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

View File

@ -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;
};

View File

@ -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) {
};

View File

@ -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;
};

View File

@ -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;*/
};

View File

@ -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
});
}
});

View File

@ -1 +0,0 @@
DICOMHP = {};

View File

@ -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;
};

View File

@ -1,5 +1,47 @@
Session.set('defaultSignInMessage', 'Tumor tracking in your browser.');
Template.lesionTrackerLayout.helpers({
fullName: function() {
return Meteor.user().profile.fullName;
},
showWorklistMenu: function() {
return Template.instance().showWorklistMenu.get();
},
currentUser: function() {
var verifyEmail = Meteor.settings && Meteor.settings.public && Meteor.settings.public.verifyEmail || false;
if (!Meteor.user() || !Meteor.userId()) {
return false;
}
if (!verifyEmail) {
return true;
}
if (Meteor.user().emails[0].verified) {
return true;
}
return false;
}
});
Template.lesionTrackerLayout.onCreated(function() {
// showViewer to go to viewer from audit
this.showWorklistMenu = new ReactiveVar(true);
// Get url and check worklist
var currentRoute = Router.current();
if (!currentRoute || !currentRoute.route) {
return;
}
var currentPath = currentRoute.route.path(this);
if (currentPath === '/' || currentPath === '/worklist') {
this.showWorklistMenu.set(false);
}
// Show countdown dialog
var handle;
$(document).on('TriggerOpenTimeoutCountdownDialog', function(e, leftTime) {

View File

@ -58,11 +58,6 @@ Package.onUse(function(api) {
bare: true
});
// Trial Criteria data validation (the Meteor package is currently out-of-date)
api.addFiles('client/compatibility/validate.js', 'client', {
bare: true
});
// UI Components
api.addFiles('client/components/lesionTrackerLayout/lesionTrackerLayout.html', 'client');
api.addFiles('client/components/lesionTrackerLayout/lesionTrackerLayout.styl', 'client');
@ -78,6 +73,10 @@ Package.onUse(function(api) {
api.addFiles('client/components/hipaaLogPage/hipaaLogPage.styl', 'client');
api.addFiles('client/components/hipaaLogPage/hipaaLogPage.js', 'client');
api.addFiles('client/components/hidingPanel/hidingPanel.html', 'client');
api.addFiles('client/components/hidingPanel/hidingPanel.styl', 'client');
api.addFiles('client/components/hidingPanel/hidingPanel.js', 'client');
api.addFiles('client/components/optionsButton/optionsButton.html', 'client');
api.addFiles('client/components/optionsModal/optionsModal.html', 'client');
@ -117,11 +116,7 @@ Package.onUse(function(api) {
api.addFiles('client/components/studyAssociationTable/studyAssociationTable.html', 'client');
api.addFiles('client/components/studyAssociationTable/studyAssociationTable.styl', 'client');
api.addFiles('client/components/studyAssociationTable/studyAssociationTable.js', 'client');
api.addFiles('client/components/confirmDeleteDialog/confirmDeleteDialog.html', 'client');
api.addFiles('client/components/confirmDeleteDialog/confirmDeleteDialog.styl', 'client');
api.addFiles('client/components/confirmDeleteDialog/confirmDeleteDialog.js', 'client');
api.addFiles('client/components/conformanceCheckFeedback/conformanceCheckFeedback.html', 'client');
api.addFiles('client/components/conformanceCheckFeedback/conformanceCheckFeedback.styl', 'client');
api.addFiles('client/components/conformanceCheckFeedback/conformanceCheckFeedback.js', 'client');

View File

@ -1,5 +1,5 @@
Package.describe({
name: "orthancremote",
name: "orthanc-remote",
summary: "Orthanc Remote",
version: '0.0.1'
});

View File

@ -7,7 +7,9 @@
right: 0
z-index: 100
width: 300px
height: 145px
min-height: 145px
max-height: 300px
height: fit-content
margin: auto
border-radius: 5px
padding: 10px 20px 10px 20px

View File

@ -1,3 +0,0 @@
<template name="login">
{{> atForm}}
</template>

View File

@ -1,3 +0,0 @@
<template name="notFound">
Not found
</template>

View File

@ -1,100 +0,0 @@
progressDialog = {
/**
* Shows progress dialog
* @param {Object} settings - Settings used by progress dialog
* @param {string} settings.title
* @param {int} settings.numberOfCompleted - The value of progress dialog
* @param {int} settings.numberOfTotal - The max value of progress dialog
* @param {Object} [settings.messageParams] - Show a message in progress dialog by setting message and messageType
* @param {string} [settings.messageParams.message="Progress"] - The message will be shown in the progress dialog
* @param {string} [settings.messageParams.messageType="info"] - Sets ui, accepts bootstrap predefined classes such as success, info, warning, danger
*/
'show': function(settings) {
// Set dialog settings
Session.set("progressDialogSettings", settings);
$('#progressDialog').css('display', 'block');
},
/**
* Updates the value of the progress dialog
* @param {int} numberOfCompleted
*/
'update': function(numberOfCompleted) {
var progressDialogSettings = Session.get("progressDialogSettings");
progressDialogSettings.numberOfCompleted = numberOfCompleted;
Session.set("progressDialogSettings", progressDialogSettings);
if (progressDialogSettings.numberOfCompleted === progressDialogSettings.numberOfTotal) {
progressDialog.close();
}
},
/**
* Closes the progress dialog
*/
'close': function() {
// Reset progressDialogSettings session
resetDialogSettingsSession();
// Close dialog
$('#progressDialog').css('display', 'none');
},
/**
* Shows a message in the progress dialog
* @param {Object} messageParams
* @param {string} messageParams.message
* @param {string} messageParams.messageType
*/
'setMessage': function(messageParams) {
var progressDialogSettings = Session.get("progressDialogSettings");
if (!messageParams.messageType || messageParams.messageType == '') {
messageParams.messageType = 'info';
}
progressDialogSettings.messageParams = messageParams;
Session.set("progressDialogSettings", progressDialogSettings);
}
};
Template.progressDialog.helpers({
'progressDialogTitle': function () {
if (Session.get("progressDialogSettings") && Session.get("progressDialogSettings").title) {
return Session.get("progressDialogSettings").title;
}
return "Progress:";
},
'progressStatus': function() {
var numberOfCompleted = 0;
if (Session.get("progressDialogSettings") && Session.get("progressDialogSettings").numberOfCompleted) {
numberOfCompleted = Session.get("progressDialogSettings").numberOfCompleted;
}
var numberofTotal = 1;
if (Session.get("progressDialogSettings") && Session.get("progressDialogSettings").numberOfTotal) {
numberofTotal = Session.get("progressDialogSettings").numberOfTotal;
}
return parseInt((numberOfCompleted / numberofTotal) * 100) + "%";
},
'progressMessage': function() {
var progressDialogSettings = Session.get("progressDialogSettings");
var messageParams = progressDialogSettings && progressDialogSettings.messageParams || false;
if (messageParams && messageParams.message) {
return '<span class="label label-'+messageParams.messageType+'">'+messageParams.message+'</span>';
}
return;
}
});
// Resets progressDialogSettings
function resetDialogSettingsSession() {
Session.set("progressDialogSettings",
{
title: 'Progress',
numberOfCompleted: 0,
numberOfTotal: 0,
messageParams: {message: null, messageType: 'info'}
}
);
}

View File

@ -1,5 +0,0 @@
<template name="studyNotFound">
<div class="studyNotFound">
<h5>Study not found</h5>
</div>
</template>

View File

@ -1,6 +0,0 @@
.studyNotFound
margin: 30px
h5
text-align: center
color: #888888

View File

@ -0,0 +1,11 @@
<template name="relatedStudySelect">
<div class="relatedStudySelect">
<label>Study:</label>
<span class="loading"><i class="fa fa-spinner fa-spin"></i></span>
<select id="selectStudyDate">
{{#each relatedStudies}}
<option value="{{studyInstanceUid}}" selected={{selected}}>{{formatDA studyDate}}: {{studyDescription}}</option>
{{/each}}
</select>
</div>
</template>

View File

@ -0,0 +1,105 @@
Template.relatedStudySelect.helpers({
/**
* Returns an array of studies that are related to the current study by patient ID.
* The value for 'selected' for the currently loaded study is set to true, so that
* this becomes the current option in the combo box.
*
* @returns {*} Array of studies that are related to the current study by patient ID
*/
relatedStudies: function() {
// Check which study is currently loaded into the study browser
var currentStudyInBrowser = ViewerStudies.findOne({
selected: true
});
// Find all studies in the Worklist which have the same patientId as the currently selected study
var relatedStudies = WorklistStudies.find({
patientId: currentStudyInBrowser.patientId
}, {
sort: {
studyDate: -1
}
}).fetch();
// If no Study / Timepoint associated studies exist, just
// return the list of loaded studies
if (!relatedStudies.length) {
return ViewerStudies.find();
}
// Modify the array of related studies so the default option is the currently selected study
relatedStudies.forEach(function(study, index) {
// If the studyInstanceUid matches that of the current study in the browser,
// Set this to 'selected', so that it becomes the default option
if (study.studyInstanceUid === currentStudyInBrowser.studyInstanceUid) {
relatedStudies[index].selected = true;
}
});
// Use this array to populate the combo box
return relatedStudies;
}
});
Template.relatedStudySelect.events({
/**
* When the study date selector combo box is changed, we will
* hide the select box, temporarily display a loading sign, and grab
* the selected study. Once the study has been retrieved it is added
* into the ViewerStudies collection and set as selected. This reactively
* populated the thumbnail browser.
*
* @param e The select box change event
*/
'change select#selectStudyDate': function(e) {
var selectBox = $(e.currentTarget);
var studyInstanceUid = selectBox.val();
// Hide the select box
selectBox.css('display', 'none');
// Show the loading indicator
var loadingIndicator = selectBox.siblings('.loading');
loadingIndicator.css('display', 'block');
getStudyMetadata(studyInstanceUid, function(study) {
sortStudy(study);
// Hide the loading indicator
loadingIndicator.css('display', 'none');
// Show the select box again
selectBox.css('display', 'block');
// Set "Selected" to false for the entire collection
ViewerStudies.update({}, {
$set: {
selected: false
}
}, {
multi: true
});
// Check if this study already exists in the ViewerStudies collection
// of loaded studies. If it does, set it's 'selected' value to true.
var existingStudy = ViewerStudies.findOne({
studyInstanceUid: studyInstanceUid
});
if (existingStudy) {
// Set the current finding in the collection to true
ViewerStudies.update(existingStudy._id, {
$set: {
selected: true
}
});
return;
}
// If the study does not exist, add the 'selected' key to the object
// with the value True, and insert it into the ViewerStudies Collection
study.selected = true;
ViewerStudies.insert(study);
});
}
});

View File

@ -0,0 +1,23 @@
.relatedStudySelect
margin: 0 auto 15px
text-align: center
width: 100px
#selectStudyDate
color: black
background-color: #666
border-color: #666
width: 96%
margin: 0 auto
label
font-weight: normal
color: #666
float: left
padding-left: 2%
.loading
display: none
color: white
text-align: center

View File

@ -2,7 +2,9 @@
<div class="studyBrowser">
<div class="scrollableStudyThumbnails">
{{ #each studies }}
{{ >thumbnails }}
{{#each thumbnails}}
{{ >thumbnailEntry }}
{{/each}}
{{ /each }}
</div>
</div>

View File

@ -1,5 +1,18 @@
Template.studyBrowser.helpers({
studies : function() {
return ViewerStudies.find({selected: true});
},
thumbnails: function() {
var study = this;
var stacks = createStacks(study);
var array = [];
stacks.forEach(function(stack, index) {
array.push({
thumbnailIndex: index,
stack: stack
});
});
return array;
}
});

View File

@ -184,9 +184,10 @@ function thumbnailDragEndHandler(e, target) {
return false;
}
var viewportIndex = $('.imageViewerViewport').index(element);
// Rerender the viewport using the drag and drop data
rerenderViewportWithNewSeries(element, OHIF.viewer.dragAndDropData);
Session.get('UseHangingProtocol', false);
layoutManager.rerenderViewportWithNewSeries(viewportIndex, OHIF.viewer.dragAndDropData);
return false;
}

View File

@ -1,5 +0,0 @@
<template name="thumbnails">
{{#each thumbnails}}
{{ >thumbnailEntry }}
{{/each}}
</template>

View File

@ -1,15 +0,0 @@
Template.thumbnails.helpers({
thumbnails: function() {
var study = this;
var stacks = createStacks(study);
var array = [];
stacks.forEach(function(stack, index) {
array.push({
thumbnailIndex: index,
stack: stack
});
});
return array;
}
});

View File

@ -19,7 +19,6 @@ function updateFramerate(rate) {
// If the movie is playing, stop/start to update the framerate
if (isPlaying()) {
var element = getActiveViewportElement();
cornerstoneTools.stopClip(element);
cornerstoneTools.playClip(element);
}

View File

@ -1,6 +1,6 @@
<template name="imageViewerViewports">
<template name="gridLayout">
<div id='imageViewerViewports'>
{{ #each viewerWindow }}
{{ #each viewports }}
<div class="viewportContainer" style="height:{{height}}%;width:{{width}}%;">
<div class="removable">
{{ >imageViewerViewport}}

View File

@ -0,0 +1,38 @@
Template.gridLayout.helpers({
height: function() {
var rows = this.rows || 1;
return 100 / rows;
},
width: function() {
var columns = this.columns || 1;
return 100 / columns;
},
viewports: function() {
var numViewports = this.rows * this.columns;
var viewportData = this.viewportData;
var numViewportsWithData = this.viewportData.length;
if (numViewportsWithData < numViewports) {
var difference = numViewports - numViewportsWithData;
for (var i = 0; i < difference; i++) {
viewportData.push({
viewportIndex: numViewportsWithData + i + 1,
rows: this.rows,
columns: this.columns
});
}
} else if (numViewportsWithData > numViewports) {
return viewportData.slice(0, numViewports);
}
return viewportData;
}
});
Template.gridLayout.onRendered(function() {
OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer('CornerstoneNewImage', cornerstoneTools.updateImageSynchronizer);
});
Template.gridLayout.onDestroyed(function() {
OHIF.viewer.updateImageSynchronizer.destroy();
});

View File

@ -1,7 +1,7 @@
#imageViewerViewports
height: calc(100% - 30px)
height: 100%
width: 100%
.viewportContainer
float: left
border: 2px solid #777

View File

@ -1,11 +0,0 @@
Template.hangingProtocolButtons.helpers({
isNextAvailable: function() {
var presentationGroup = Session.get('WindowManagerPresentationGroup');
var numPresentationGroups = WindowManager.getNumPresentationGroups();
return presentationGroup < numPresentationGroups;
},
isPreviousAvailable: function() {
var presentationGroup = Session.get('WindowManagerPresentationGroup');
return presentationGroup > 1;
}
});

View File

@ -21,6 +21,7 @@ function loadSeriesIntoViewport(data, templateData) {
// The viewport index is often used to store information about a viewport element
var element = data.element;
var viewportIndex = $('.imageViewerViewport').index(element);
layoutManager.viewportData[viewportIndex].viewportIndex = viewportIndex;
// Get the contentID of the current worklist tab, if the viewport is running
// alongside the worklist package
@ -140,15 +141,47 @@ function loadSeriesIntoViewport(data, templateData) {
return;
}
// If there is a saved object containing Cornerstone viewport data
// (e.g. scale, invert, window settings) in the input data, apply it now.
//
// Otherwise, display the loaded image in the viewport element with the
// default viewport settings.
if (data.viewport) {
// Update the enabled element with the image and viewport data
// This is not usually necessary, but we need them stored in case
// a sopClassUid-specific viewport setting is present.
enabledElement.image = image;
enabledElement.viewport = cornerstone.getDefaultViewport(enabledElement.canvas, image);
// Check if there are default viewport settings for this sopClassUid
if (!series.instances || !series.instances.length) {
return;
}
var instance = series.instances[0];
var instanceClassViewport = getInstanceClassDefaultViewport(instance, enabledElement, image.imageId);
// If there are sopClassUid-specific viewport settings, apply them
if (instanceClassViewport) {
cornerstone.displayImage(element, image, instanceClassViewport);
// Mark that this element should not be fit to the window in the resize listeners
// TODO: Find another way to do this?
enabledElement.fitToWindow = false;
// Resize the canvas to fit the current viewport element size.
cornerstone.resize(element, false);
} else if (data.viewport) {
// If there is a saved object containing Cornerstone viewport data
// (e.g. scale, invert, window settings) in the input data, apply it now.
cornerstone.displayImage(element, image, data.viewport);
// Resize the canvas to fit the current viewport element size. Fit the displayed
// image to the canvas dimensions.
cornerstone.resize(element, true);
} else {
// If no saved viewport settings or modality-specific settings exists,
// display the loaded image in the viewport element with no loaded viewport
// settings.
cornerstone.displayImage(element, image);
// Resize the canvas to fit the current viewport element size. Fit the displayed
// image to the canvas dimensions.
cornerstone.resize(element, true);
}
// Remove the data for this viewport from the ViewportLoading object
@ -159,10 +192,6 @@ function loadSeriesIntoViewport(data, templateData) {
// (e.g. hide the progress text box)
endLoadingHandler(element);
// Resize the canvas to fit the current viewport element size. Fit the displayed
// image to the canvas dimensions.
cornerstone.resize(element, true);
// Remove the 'empty' class from the viewport to hide any instruction text
element.classList.remove('empty');
@ -172,7 +201,7 @@ function loadSeriesIntoViewport(data, templateData) {
$(element).siblings('.imageViewerViewportOverlay').show();
// Add stack state managers for the stack tool, CINE tool, and reference lines
cornerstoneTools.addStackStateManager(element, [ 'stack', 'playClip', 'referenceLines' ]);
cornerstoneTools.addStackStateManager(element, ['stack', 'playClip', 'referenceLines']);
// Get the current viewport settings
var viewport = cornerstone.getViewport(element);
@ -206,7 +235,7 @@ function loadSeriesIntoViewport(data, templateData) {
// Use the tool manager to enable the currently active tool for this
// newly rendered element
var activeTool = toolManager.getActiveTool();
toolManager.setActiveTool(activeTool, [ element ]);
toolManager.setActiveTool(activeTool, [element]);
// Define a function to run whenever the Cornerstone viewport is rendered
// (e.g. following a change of window or zoom)
@ -220,6 +249,7 @@ function loadSeriesIntoViewport(data, templateData) {
// Save the current viewport into the ViewerData global variable, as well as the
// Meteor Session. This lets the viewport be saved/reloaded on a hot-code reload
var viewport = cornerstone.getViewport(element);
layoutManager.viewportData[viewportIndex].viewport = viewport;
ViewerData[contentId].loadedSeriesData[viewportIndex].viewport = viewport;
Session.set('ViewerData', ViewerData);
}
@ -240,6 +270,7 @@ function loadSeriesIntoViewport(data, templateData) {
// This allows the template helpers to update reactively
templateData.imageId = eventData.enabledElement.image.imageId;
Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
layoutManager.viewportData[viewportIndex].imageId = eventData.enabledElement.image.imageId;
// Get the element and stack data
var element = e.target;
@ -260,6 +291,7 @@ function loadSeriesIntoViewport(data, templateData) {
var stack = cornerstoneTools.getToolState(element, 'stack');
if (stack && stack.data.length && stack.data[0].imageIds.length > 1) {
var imageIdIndex = stack.data[0].imageIds.indexOf(templateData.imageId);
layoutManager.viewportData[viewportIndex].currentImageIdIndex = imageIdIndex;
ViewerData[contentId].loadedSeriesData[viewportIndex].currentImageIdIndex = imageIdIndex;
Session.set('ViewerData', ViewerData);
}
@ -327,14 +359,17 @@ function loadSeriesIntoViewport(data, templateData) {
$(element).off(allCornerstoneEvents, sendActivationTrigger);
$(element).on(allCornerstoneEvents, sendActivationTrigger);
// Store the current series data inside a global variable
OHIF.viewer.loadedSeriesData[viewportIndex] = {
// Store the current series data inside the Layout Manager
layoutManager.viewportData[viewportIndex] = {
imageId: imageId,
studyInstanceUid: data.studyInstanceUid,
seriesInstanceUid: data.seriesInstanceUid,
currentImageIdIndex: data.currentImageIdIndex,
viewport: viewport
viewport: viewport,
viewportIndex: viewportIndex
};
ViewerData[contentId].loadedSeriesData = OHIF.viewer.loadedSeriesData;
ViewerData[contentId].loadedSeriesData = layoutManager.viewportData;
// Check if image plane (orientation / loction) data is present for the current image
var imagePlane = cornerstoneTools.metaData.get('imagePlane', image.imageId);
@ -443,7 +478,7 @@ Meteor.startup(function() {
});
Template.imageViewerViewport.onCreated(function() {
log.info('imageViewerViewport onCreated');
log.info('imageViewerViewport onCreated');
});
Template.imageViewerViewport.onRendered(function() {
@ -487,23 +522,6 @@ Template.imageViewerViewport.onRendered(function() {
});
var seriesInstanceUid = this.data.seriesInstanceUid;
// TODO: This code block might be refactored
// Load previous measurement study when reloading a patient
if (!study) {
getStudyMetadata(this.data.studyInstanceUid, function(study) {
// Once we have retrieved the data, we sort the series' by series
// and instance number in ascending order
if (!study) {
return;
}
sortStudy(study);
data.study = study;
setSeries(data, seriesInstanceUid, templateData);
})
}
data.study = study;
setSeries(data, seriesInstanceUid, templateData);
});
@ -539,5 +557,9 @@ Template.imageViewerViewport.events({
'click .imageViewerViewport': function(e) {
var viewportIndex = $('.imageViewerViewport').index(e.currentTarget);
Session.set('activeViewport', viewportIndex);
},
'dblclick .imageViewerViewport': function(e) {
var viewportIndex = $('.imageViewerViewport').index(e.currentTarget);
layoutManager.toggleEnlargement(viewportIndex);
}
});
});

View File

@ -1,201 +0,0 @@
ViewerWindows = new Meteor.Collection(null);
ViewerWindows._debugName = 'ViewerWindows';
Template.imageViewerViewports.helpers({
height: function() {
var viewportRows = this.viewportRows || 1;
return 100 / viewportRows;
},
width: function() {
var viewportColumns = this.viewportColumns || 1;
return 100 / viewportColumns;
},
viewerWindow: function() {
log.info('ViewerWindows');
//log.info(ViewerWindows.find().fetch());
ViewerWindows.remove({});
log.info('imageViewerViewports viewportArray');
var viewportRows = this.viewportRows || 1;
var viewportColumns = this.viewportColumns || 1;
var contentId = Session.get('activeContentId');
if (this.viewportRows && this.viewportColumns) {
viewportRows = this.viewportRows || 1;
viewportColumns = this.viewportColumns || 1;
} else if (ViewerData[contentId].viewportRows && ViewerData[contentId].viewportColumns) {
viewportRows = ViewerData[contentId].viewportRows;
viewportColumns = ViewerData[contentId].viewportColumns;
}
var viewportData;
if (!$.isEmptyObject(ViewerData[contentId].loadedSeriesData)) {
viewportData = ViewerData[contentId].loadedSeriesData;
}
var inputData = {
viewportColumns: viewportColumns,
viewportRows: viewportRows
};
inputData.DisplaySetPresentationGroup = Session.get('WindowManagerPresentationGroup');
var hangingProtocolViewportData = WindowManager.getHangingProtocol(inputData);
if (Session.get('UseHangingProtocol')) {
viewportData = hangingProtocolViewportData.viewports;
viewportRows = hangingProtocolViewportData.viewportRows || viewportRows;
viewportColumns = hangingProtocolViewportData.viewportColumns || viewportColumns;
}
// Update viewerData
ViewerData[contentId].viewportRows = viewportRows;
ViewerData[contentId].viewportColumns = viewportColumns;
Session.set('ViewerData', ViewerData);
this.viewportRows = viewportRows;
this.viewportColumns = viewportColumns;
var numViewports = viewportRows * viewportColumns;
for (var i = 0; i < numViewports; ++i) {
var data = {
viewportIndex: i,
// These two are necessary because otherwise the width and height helpers
// don't get the right data context. Seems to be related to the "each" loop.
viewportColumns: viewportColumns,
viewportRows: viewportRows
};
if (viewportData && !$.isEmptyObject(viewportData[i])) {
data.seriesInstanceUid = viewportData[i].seriesInstanceUid;
data.studyInstanceUid = viewportData[i].studyInstanceUid;
data.currentImageIdIndex = viewportData[i].currentImageIdIndex;
data.viewport = viewportData[i].viewport;
} else if (hangingProtocolViewportData && !$.isEmptyObject(hangingProtocolViewportData.viewports[i])) {
data.seriesInstanceUid = hangingProtocolViewportData.viewports[i].seriesInstanceUid;
data.studyInstanceUid = hangingProtocolViewportData.viewports[i].studyInstanceUid;
data.currentImageIdIndex = hangingProtocolViewportData.viewports[i].currentImageIdIndex;
data.viewport = hangingProtocolViewportData.viewports[i].viewport;
}
ViewerWindows.insert(data);
}
// Here we will find out if we need to load any other studies into the viewer
// We will make a list of unique studyInstanceUids
var uniqueStudyInstanceUids = [];
// Meteor doesn't support Mongo's 'distinct' function, so we have to do this in a loop
var windows = ViewerWindows.find({}, {
reactive: false
}).fetch();
windows.forEach(function(window) {
var studyInstanceUid = window.studyInstanceUid;
if (!studyInstanceUid) {
return;
}
// If this studyInstanceUid is already in the list, stop here
if (uniqueStudyInstanceUids.indexOf(studyInstanceUid) > -1) {
return;
}
// Otherwise, add it to the list
uniqueStudyInstanceUids.push(studyInstanceUid);
// If any of the associated studies is not already loaded, load it now
var loadedStudy = ViewerStudies.findOne({
studyInstanceUid: studyInstanceUid
}, {
reactive: false
});
if (!loadedStudy) {
// Load the study
getStudyMetadata(studyInstanceUid, function(study) {
log.info("imageViewerViewports GetStudyMetadata: " + studyInstanceUid);
// Sort the study's series and instances by series and instance number
sortStudy(study);
// Insert it into the ViewerStudies Collection
ViewerStudies.insert(study);
});
}
});
return ViewerWindows.find({}, {
reactive: false
}).fetch();
}
});
var savedSeriesData,
savedViewportRows,
savedViewportColumns;
Template.imageViewerViewports.events({
'CornerstoneMouseDoubleClick .imageViewerViewport': function(e) {
var container = $('.viewerMain').get(0);
var data;
var contentId = this.contentId || $('#viewer').parents('.tab-pane.active').attr('id');
// If there is more than one viewport on screen
// And one of them is double-clicked, it should be rendered alone
// If it is double-clicked again, the viewer should revert to the previous layout
if ($(e.currentTarget).hasClass('zoomed')) {
// Revert to saved settings
ViewerData[contentId].loadedSeriesData = $.extend(true, {}, savedSeriesData);
ViewerData[contentId].viewportRows = savedViewportRows;
ViewerData[contentId].viewportColumns = savedViewportColumns;
savedViewportRows = 0;
savedViewportColumns = 0;
data = {
viewportRows: ViewerData[contentId].viewportRows,
viewportColumns: ViewerData[contentId].viewportColumns
};
// Render the imageViewerViewports template with these settings
$('#imageViewerViewports').remove();
UI.renderWithData(Template.imageViewerViewports, data, container);
// Remove the 'zoomed' class from any viewports
$('.imageViewerViewport').removeClass('zoomed');
} else {
// Zoom to single viewport
// If only one viewport is on-screen, stop here
if (ViewerData[contentId].viewportRows === 1 &&
ViewerData[contentId].viewportColumns === 1) {
return;
}
// Save the current settings
savedSeriesData = $.extend(true, {}, ViewerData[contentId].loadedSeriesData);
savedViewportRows = ViewerData[contentId].viewportRows;
savedViewportColumns = ViewerData[contentId].viewportColumns;
// Get the clicked-on viewport's index
var viewportIndex = this.viewportIndex;
// Set the first viewport's data to be the same as the currently clicked-on viewport
ViewerData[contentId].loadedSeriesData[0] = ViewerData[contentId].loadedSeriesData[viewportIndex];
// Set the basic template data
data = {
viewportRows: 1,
viewportColumns: 1
};
// Render the imageViewerViewports template with these settings
$('#imageViewerViewports').remove();
UI.renderWithData(Template.imageViewerViewports, data, container);
// Add the 'zoomed' class to the lone remaining viewport
$('.imageViewerViewport').eq(0).addClass('zoomed');
}
}
});

View File

@ -1,6 +0,0 @@
<template name="layoutButton">
<button id="layout" type="button" class="btn btn-sm btn-default dropdown-toggle" data-container="body" data-toggle="dropdown" aria-expanded="false" data-placement="right" title="Layout" rel="tooltip">
<span class="fa fa-th-large"></span>
</button>
{{ >layoutChooser }}
</template>

Some files were not shown because too many files have changed in this diff Show More