Lesion Tracker Redesign so far

This commit is contained in:
Erik Ziegler committed 2016-07-22 11:00:16 +02:00
1 parent b247e89d8b
commit a9a4f3f82b
109 files changed
+2972 -936

No files matched your search

@@ -0,0 +1,77 @@
<template name="additionalMeasurements">
<div id="additionalMeasurements">
<div class="scrollArea">
<div class="section">
<div class="header">
Additional Findings: Baseline
</div>
<div>
{{ >radioOptionGroup measurableDiseaseButtonData}}
</div>
</div>
<div class="section">
<div class="header">
Supplementary Measurements
</div>
<div class="entrySection">
<label>Number of bone lesions</label>
<div>
<select>
<option>0</option>
<option>1-2</option>
<option selected>2-4</option>
<option>>5</option>
</select>
</div>
</div>
<div class="entrySection">
<label>Regions of metastatic disease</label>
<select id="regionsOfMetastaticDisease">
<option>None</option>
<option>Lymph node</option>
</select>
</div>
{{ >radioOptionGroup metastaticDiseaseButtonData}}
</div>
<div class="section">
<div class="header">
Image Quality
</div>
{{ >radioOptionGroup imageQualityButtonData}}
{{ >radioOptionGroup anatomicalCoverageButtonData}}
{{ >radioOptionGroup presenceOfContrastButtonData}}
</div>
<div class="section">
<div class="header">
Additional Screen Captures
</div>
<div class="entrySection buttonSection">
<div class="svgContainer">
<svg>
<use xlink:href="/packages/lesiontracker/assets/icons.svg#icon-create-screen-capture"></use>
</svg>
</div>
<div class="buttonLabel noselect">
Add new screen capture
</div>
</div>
</div>
<div class="section">
<div class="header">
Comments
</div>
<div class="entrySection buttonSection">
<div class="svgContainer">
<svg>
<use xlink:href="/packages/lesiontracker/assets/icons.svg#icon-create-comment"></use>
</svg>
</div>
<div class="buttonLabel noselect">
Add a comment
</div>
</div>
</div>
</div>
</div>
</template>
@@ -0,0 +1,87 @@
var yesNoOptions = [{
key: 'yes',
text: 'Yes'
}, {
key: 'no',
text: 'No'
}];
Template.additionalMeasurements.onCreated(function additionalMeasurementsOnCreated() {
const instance = this;
instance.measurableDisease = new ReactiveVar();
instance.numberOfBoneLesions = new ReactiveVar();
instance.regionsOfMetastaticDisease = new ReactiveVar();
instance.tracerRelatedToMetastaticDisease = new ReactiveVar();
instance.acceptableImageQuality = new ReactiveVar();
instance.adequateAnatomicalCoverage = new ReactiveVar();
instance.presenceOfContrast = new ReactiveVar();
instance.phaseOfContrast = new ReactiveVar();
instance.measurableDisease.set('absent');
instance.tracerRelatedToMetastaticDisease.set('no');
instance.acceptableImageQuality.set('yes');
instance.adequateAnatomicalCoverage.set('yes');
instance.presenceOfContrast.set('yes');
});
Template.additionalMeasurements.onRendered(function additionalMeasurementsOnRendered() {
const instance = this;
instance.$("#regionsOfMetastaticDisease").select2({
multiple: true
});
});
Template.additionalMeasurements.helpers({
measurableDiseaseButtonData() {
const instance = Template.instance();
return {
text: 'Measurable disease',
value: instance.measurableDisease,
options: [{
key: 'present',
text: 'Present'
}, {
key: 'absent',
text: 'Absent'
}]
};
},
metastaticDiseaseButtonData() {
const instance = Template.instance();
return {
text: 'Tracer related to metastatic disease',
value: instance.tracerRelatedToMetastaticDisease,
options: yesNoOptions
};
},
imageQualityButtonData() {
const instance = Template.instance();
return {
text: 'Acceptable image quality',
value: instance.acceptableImageQuality,
options: yesNoOptions
};
},
anatomicalCoverageButtonData() {
const instance = Template.instance();
return {
text: 'Adequate anatomical coverage',
value: instance.adequateAnatomicalCoverage,
options: yesNoOptions
};
},
presenceOfContrastButtonData() {
const instance = Template.instance();
return {
text: 'Presence of contrast',
value: instance.presenceOfContrast,
options: yesNoOptions
};
}
});
@@ -0,0 +1,74 @@
@import "{design}/app"
#additionalMeasurements
width: 100%
height: 100%
background-color: #000000
.scrollArea
height: 100%
overflow-x: hidden
overflow-y: auto
margin-right: -16px
padding-bottom: 100px
padding-right: 16px
-webkit-overflow-scrolling: touch
&::-webkit-scrollbar
display: none
.header
height: 33px
line-height: 33px
font-weight: 400
font-size: 14px
padding: 0 10px
color: $textPrimaryColor
background-color: $darkUiColor
.entrySection
#regionsOfMetastaticDisease
width: 285px
height: 30px
border-radius: 2px
background-color: #b6b6b6
color: #3e3e3e
font-size: 14px
.buttonSection
cursor: pointer
height: 100px
color: $defaultColor
&:hover
color: $hoverColor
.svgContainer
svg
fill: $hoverColor
&:active
color: $activeColor
.svgContainer
svg
fill: $activeColor
div
float: left
display: inline-block
height: 80px
.buttonLabel
line-height: 80px
padding: 0 30px
.svgContainer
padding: 15px 5px
width: 40px
svg
width: 40px
height: 40px
fill: $textPrimaryColor
@@ -0,0 +1,16 @@
<template name="radioOptionGroup">
<div class="{{class}} entrySection viewerRadioOptionGroup">
<h5>{{text}}</h5>
<div>
{{#each item in this.options}}
<label>
<input type="radio"
name="{{item.key}}"
data-key="{{item.key}}"
{{inlineIf value.get item.key 'selected'}}/>
{{item.text}}
</label>
{{/each}}
</div>
</div>
</template>
@@ -0,0 +1,15 @@
Template.radioOptionGroup.onRendered(() => {
const instance = Template.instance();
const value = instance.data.value;
if (!value.get()) {
value.set(instance.data.options[0].key);
}
});
Template.radioOptionGroup.events({
'click [data-key]'(event, instance) {
const $element = $(event.currentTarget);
const key = $element.attr('data-key');
instance.data.value.set(key);
}
});
@@ -0,0 +1,20 @@
@import "{design}/app"
.entrySection
padding: 10px
color: $textPrimaryColor
&:not(:last-child)
border-bottom: $uiBorderThickness solid $uiBorderColorDark
h5
font-weight: 300
label
font-size: 14px
font-weight: 100
display: inline-block
padding: 5px
input
margin: 5px
@@ -0,0 +1,11 @@
<template name="caseProgress">
<div class="caseProgress">
{{ >radialProgressBar isLocked=isLocked progressPercent=progressPercent progressText=progressText}}
{{ #if isLocked }}
<h5>Locked</h5>
{{ /if }}
{{ #if progressComplete }}
<button class="btn">Enter response</button>
{{ /if }}
</div>
</template>
@@ -0,0 +1,30 @@
Template.caseProgress.onCreated(function caseProgressOnCreated() {
const instance = Template.instance();
instance.progressPercent = new ReactiveVar();
instance.progressPercent.set(50);
instance.progressText = new ReactiveVar();
instance.progressText.set(5);
instance.isLocked = new ReactiveVar(false);
});
Template.caseProgress.helpers({
progressPercent() {
return Template.instance().progressPercent.get();
},
progressText() {
return Template.instance().progressText.get();
},
isLocked() {
return Template.instance().isLocked.get();
},
progressComplete() {
var progressPercent = Template.instance().progressPercent.get();
return progressPercent === 100;
}
});
@@ -0,0 +1,3 @@
@import "{design}/app"
//.caseProgress
@@ -0,0 +1,20 @@
<template name="flexboxLayout">
<div class="viewerSection">
<div class="sidebarMenu sidebar-left {{ #if studySidebarOpen }}sidebar-open{{/if}}">
<div class="timepointButtonContainer p-t-2">
{{>roundedButtonGroup buttonGroupData}}
</div>
<hr class="m-y-0">
{{>studyTimepointBrowser timepointViewType=timepointViewType}}
</div>
<div class="mainContent {{ #if studySidebarOpen }}sidebar-left-open{{/if}} {{ #if rightSidebarOpen }}sidebar-right-open{{/if}}">
{{>viewerMain }}
</div>
<div class="sidebarMenu sidebar-right {{ #if lesionSidebarOpen }}sidebar-open{{/if}}">
{{>lesionTable }}
</div>
<div class="sidebarMenu sidebar-right {{ #if additionalMeasurementsSidebarOpen }}sidebar-open{{/if}}">
{{>additionalMeasurements }}
</div>
</div>
</template>
@@ -0,0 +1,66 @@
Template.flexboxLayout.onCreated(() => {
const instance = Template.instance();
instance.state = instance.data.state;
instance.timepointViewType = new ReactiveVar();
});
var resizeTimeout;
Template.flexboxLayout.onRendered(() => {
const instance = Template.instance();
instance.autorun(function() {
Meteor.clearTimeout(resizeTimeout);
// Trigger a resize any time the layout state changes
var studySidebarOpen = instance.state.get('studySidebarOpen');
var lesionSidebarOpen = instance.state.get('lesionSidebarOpen');
var additionalMeasurementsSidebarOpen = instance.state.get('additionalMeasurementsSidebarOpen');
resizeTimeout = Meteor.setTimeout(function() {
handleResize();
}, 300);
});
});
Template.flexboxLayout.helpers({
timepointViewType() {
return Template.instance().timepointViewType;
},
buttonGroupData() {
const instance = Template.instance();
return {
value: instance.timepointViewType,
options: [{
key: 'key',
text: 'Key Timepoints'
}, {
key: 'all',
text: 'All Timepoints'
}]
};
},
studySidebarOpen() {
const instance = Template.instance();
return instance.state.get('studySidebarOpen');
},
lesionSidebarOpen() {
const instance = Template.instance();
return instance.state.get('lesionSidebarOpen');
},
additionalMeasurementsSidebarOpen() {
const instance = Template.instance();
return instance.state.get('additionalMeasurementsSidebarOpen');
},
rightSidebarOpen() {
const instance = Template.instance();
const lesionSidebarOpen = instance.data.state.get('lesionSidebarOpen');
const additionalMeasurementsSidebarOpen = instance.data.state.get('additionalMeasurementsSidebarOpen');
return lesionSidebarOpen || additionalMeasurementsSidebarOpen;
}
});
@@ -0,0 +1,69 @@
@import "{design}/app"
.viewerSection
display: inline-flex
flex-flow: row nowrap
align-items: stretch
height: 'calc(100% - %s)' % $toolbarHeight
width: 100%
.sidebarMenu
position: absolute
height: 100%
background: $primaryBackgroundColor
-webkit-transition: $sidebarTransition
-ms-transition: $sidebarTransition
-o-transition: $sidebarTransition
-moz-transition: $sidebarTransition
transition: $sidebarTransition
.timepointButtonContainer
height: 55px
.sidebar-left
width: $studiesSidebarMenuWidth
order: 1
flex: 0
left: - $studiesSidebarMenuWidth
border-right: $uiBorderThickness $uiBorderColor solid
&.sidebar-open
left: 0
.mainContent
order: 2
width: 100%
height: 100%
-webkit-transition: $sidebarTransition
-ms-transition: $sidebarTransition
-o-transition: $sidebarTransition
-moz-transition: $sidebarTransition
transition: $sidebarTransition
transform: translate3d(0,0,0);
&.sidebar-left-open.sidebar-right-open
width: 'calc(100% - %s)' % ($studiesSidebarMenuWidth + $lesionsSidebarMenuWidth)
&.sidebar-right-open
width: 'calc(100% - %s)' % $lesionsSidebarMenuWidth
&.sidebar-left-open
width: 'calc(100% - %s)' % $studiesSidebarMenuWidth
&.sidebar-left-open
transform: translate3d($studiesSidebarMenuWidth,0,0);
.sidebar-right
width: $lesionsSidebarMenuWidth
order: 3
flex: 0
right: - $lesionsSidebarMenuWidth
border-left: $uiBorderThickness $uiBorderColor solid
&.sidebar-open
right: 0
.studiesListedChanger
padding: 20px 10px
text-align: center
border-bottom: $uiBorderThickness $uiBorderColor solid
@@ -9,9 +9,7 @@
</button>
</div>
<div class="hidingPanelContent">
{{#if studyDateIsShown}}
{{> studyDateList}}
{{/if}}
{{> studyDateList}}
<div class="studyBrowserContainer">
{{> studyBrowser}}
</div>
@@ -1,6 +1,6 @@
function pinPanel() {
var hidingPanel = $(".hidingPanel");
var hidingPanel = $('.hidingPanel');
hidingPanel.css({
width: '122px'
});
@@ -12,12 +12,12 @@ function pinPanel() {
// Calculate newWidth of viewportAndLesionTable
var viewerWidth = $('#viewer').width();
var newWidth = 100 - 122 / viewerWidth * 100;
$("#viewportAndLesionTable").css('width', newWidth + '%');
$('#viewportAndLesionTable').css('width', newWidth + '%');
}
function unpinPanel() {
// Calculate newWidth of viewportAndLesionTable
$("#viewportAndLesionTable").css('width', '99%');
// Calculate newWidth of viewportAndLesionTable
$('#viewportAndLesionTable').css('width', '99%');
}
Template.hidingPanel.events({
@@ -90,9 +90,6 @@ Template.hidingPanel.events({
});
Template.hidingPanel.helpers({
studyDateIsShown: function() {
return true;
},
isPinned: function() {
return Template.instance().isPinned.get();
}
@@ -117,7 +114,7 @@ Template.hidingPanel.onCreated(function() {
if (!isPanelPinned) {
isPanelPinned = false;
}
this.isPinned = new ReactiveVar(isPanelPinned);
});
@@ -1,27 +1,30 @@
<template name="lesionTable">
<div id="lesionTableContainer">
<div id="dragbar"></div>
<div id="lesionTableWrapper">
<table class="table table-striped noselect lesionTable" id="tblLesion">
<thead>
<tr>
<th id="thNumber">Lesion #</th>
<!--<th id="thLocation">Location</th>-->
<th id="thTarget">Target</th>
<!--Each time point name as a column-->
{{ #each timepoints }}
{{ >lesionTableTimepointHeader }}
{{ /each }}
</tr>
</thead>
<tbody>
<!--Each measurement as row-->
{{ #each measurement }}
{{ >lesionTableRow }}
{{ /each }}
</tbody>
</table>
<div class="lesionTableLayoutChanger viewerRoundedButtonGroup">
{{ >roundedButtonGroup buttonGroupData }}
</div>
<div class="lesionTableTimepointHeaderRow">
<div class="spacer">
</div>
{{ #each timepoints }}
{{ >lesionTableTimepointHeader }}
{{ /each }}
</div>
<div class="lesionTableWrapper">
{{ >lesionTableHeaderRow id="target" type="Targets" measurements=targets}}
{{ #each targets }}
{{ >lesionTableRow }}
{{ /each }}
{{ >lesionTableHeaderRow id="nonTarget" type="Non-targets" measurements=nonTargets}}
{{ #each nonTargets }}
{{ >lesionTableRow }}
{{ /each }}
{{ >lesionTableHeaderRow id="newLesions" type="New lesions" measurements=newLesions}}
{{ #each nonTargets }}
{{ >lesionTableRow }}
{{ /each }}
</div>
</div>
</template>
@@ -1,9 +1,30 @@
Template.lesionTable.helpers({
measurement: function() {
targets: function() {
// All Targets shall be listed first followed by Non-Targets
return Measurements.find({}, {
return Measurements.find({
isTarget: true
}, {
sort: {
lesionNumberAbsolute: 1
}
});
},
nonTargets: function() {
// All Targets shall be listed first followed by Non-Targets
return Measurements.find({
isTarget: false
}, {
sort: {
lesionNumberAbsolute: 1
}
});
},
newLesions: function() {
// All Targets shall be listed first followed by Non-Targets
return Measurements.find({
saved: false
}, {
sort: {
isTarget: -1,
lesionNumberAbsolute: 1
}
});
@@ -14,59 +35,43 @@ Template.lesionTable.helpers({
latestDate: 1
}
});
},
buttonGroupData() {
const instance = Template.instance();
return {
value: instance.lesionTableLayout,
options: [{
key: 'comparison',
text: 'Comparison'
}, {
key: 'key',
text: 'Key Timepoints'
}]
};
}
});
Template.lesionTable.events({
// 'click table#tblLesion tbody tr': function(e, template) {
// Retrieve the lesion id from the DOM data for this row
// var measurementId = $(e.currentTarget).data('measurementid');
// activateLesion(measurementId, template.data);
//},
'mousedown div#dragbar': function(e, template) {
var pY = e.pageY;
var draggableParent = $(e.currentTarget).parent();
var startHeight = draggableParent.height();
template.dragging.set(true);
$(document).on('mouseup', function(e) {
template.dragging.set(false);
$(document).off('mouseup').off('mousemove');
});
$(document).on('mousemove', function(e) {
var topPosition = e.pageY - pY;
var newHeight = startHeight - topPosition;
// Min lesion table height = 5px
if (newHeight < 5) {
return;
}
draggableParent.css({
top: topPosition,
height: newHeight
});
var viewportAndLesionTableHeight = $('#viewportAndLesionTable').height();
var newPercentageHeightofLesionTable = (startHeight - topPosition) / viewportAndLesionTableHeight * 100;
var newPercentageHeightofViewermain = 100 - newPercentageHeightofLesionTable;
$('.viewerMain').height(newPercentageHeightofViewermain + '%');
// Resize viewport
resizeViewportElements();
});
}
});
Template.lesionTable.onCreated(function() {
this.dragging = new ReactiveVar(false);
/**
* Retrieve the lesion id from the DOM data for this row
*/
/*'click table#tblLesion tbody tr': function(e, template) {
var measurementId = $(e.currentTarget).data('measurementid');
activateLesion(measurementId, template.data);
},*/
});
// Temporary until we have a real window manager with events for series/study changed
Session.setDefault('NewSeriesLoaded', false);
Template.lesionTable.onCreated(function() {
var instance = this;
instance.lesionTableLayout = new ReactiveVar();
instance.lesionTableLayout.set('comparisonTimepoints');
});
Template.lesionTable.onRendered(function() {
// Find the first measurement by Lesion Number
var firstLesion = Measurements.findOne({}, {
@@ -1,93 +1,23 @@
@import "{design}/app"
#lesionTableContainer
width: 100%
height: 16%
background-color: #000000
#dragbar
width: 100%
height: 3px
top: 0
left: 0
right: 0
bottom: 0
background-color: #666666
cursor: row-resize
#lesionTableWrapper
position: relative
width: 100%
height: 100%
margin: 0 auto
.lesionTable
.lesionTableTimepointHeaderRow
display: flex
td, th
border: 1px solid #FFFFFF
border-collapse: collapse
width: 100px
.spacer
width: 20px
td.lesionNumber, th#thNumber
width: 70px
td.location, th#thLocation
width: 250px
td.target, th#thTarget
width: 70px
thead
width: 100%
height: 40px
display: block
tr
width: 100%
height: 40px
th
border-bottom-width: 0
font-weight: normal
color: #FFFFFF
line-height: 15px
text-align: center
margin: 0
padding: 5px
background-color: #424242
vertical-align: middle
tbody
display: block
position: absolute
top: 40px
left: 0
right: 0
bottom: 0
overflow: auto
width: 100%
padding-bottom: 20px
tr.selectedRow
background: #F5F5F5 !important
color: #000000 !important
tr.lesionTableRow
width: 100%
&:nth-child(even)
background: #888
&:nth-child(odd)
background: #BBB
&:hover
cursor: pointer
background-color: #009BD2
color: white
td
border-bottom-width: 0
margin: 0
padding: 5px
line-height: 15px
&.lesionNumber, &.target, &.lesionTableTimepointCell
text-align: center
.lesionTableLayoutChanger
padding: 5px
text-align: center
margin: 0 auto
@@ -0,0 +1,22 @@
<template name="lesionTableHeaderRow">
<div class="lesionTableHeaderRow">
<div class="add">
<svg>
<use xlink:href=/packages/lesiontracker/assets/icons.svg#icon-ui-add></use>
</svg>
</div>
<div class="type">
{{type}}
</div>
<div class="max">
{{ #if maxNumLesions }}
<p class="maxNumLesions">
Max {{maxNumLesions}}
</p>
{{/if}}
</div>
<div class="numberOfLesions">
{{numberOfLesions}}
</div>
</div>
</template>
@@ -0,0 +1,38 @@
Template.lesionTableHeaderRow.helpers({
numberOfLesions: function() {
var instance = Template.instance();
return instance.data.measurements.count();
},
maxNumLesions: function() {
var instance = Template.instance();
var lesionType = instance.id;
// TODO: Check what we are annotating
var timepointType = 'baseline';
// Identify which Trial Conformance Criteria are currently being used
// Note that there may be more than one.
var criteriaTypes = TrialCriteriaTypes.find({
selected: true
}).map(function(criteria) {
return criteria.id;
});
var currentConstraints = getTrialCriteriaConstraints(criteriaTypes);
if (!currentConstraints) {
// For testing
return 10;
}
var criteria = currentConstraints[timepointType][lesionType];
return criteria.group.totalNumberOfLesions;
}
});
Template.lesionTableHeaderRow.events({
'click .add': function() {
console.log('Add was clicked');
// TODO: Set active tool to new type
}
});
@@ -0,0 +1,58 @@
@import "{design}/app"
$headerRowHeight = 63px
.lesionTableHeaderRow
background: $darkUiColor
color: $textSecondaryColor
fill: $textSecondaryColor
width: 100%
height: $headerRowHeight
display: flex
line-height: $headerRowHeight
div
flex: 1
justify-content: space-around
align-items: stretch
text-align: center
.add
padding: 0 10px
max-width: 20px
cursor: pointer
svg
height: $headerRowHeight
max-width: 11px
.type
cursor: pointer
font-weight: 200
height: $headerRowHeight
font-size: 13pt
.numberOfLesions
float: right
font-weight: 100
font-size: 40px
max-width: 30px
margin-right: 5px
height: $headerRowHeight
line-height: $headerRowHeight
.max
height: $headerRowHeight
background: $darkUiColor
.maxNumLesions
margin: 21px auto
height: 20px
line-height: 20px
width: 47px
font-size: 12px
text-transform: uppercase
font-weight: 500
border-radius: 3px
color: black
background-color: $textSecondaryColor;
@@ -1,11 +1,38 @@
<template name="lesionTableRow">
<tr id="{{lesionNumber}}" class="lesionTableRow" data-measurementid="{{_id}}">
<td class='lesionNumber'>{{lesionNumberAbsolute}}</td>
<!--<td class='location' tabindex="0">{{location}}</td>-->
<td class='target'>{{#if isTarget}} Y {{else}} N {{/if}}</td>
<!--Each time point as a column-->
{{# each timepoints }}
{{> lesionTableTimepointCell}}
{{/ each }}
</tr>
<div class="lesionTableRow" data-measurementid="{{_id}}">
<div class='lesionRowSidebar'>
<div class="lesionNumber">
{{lesionNumberAbsolute}}
</div>
<div class="lesionNumber">
{{ #if descriptionRequired }}
<div>
<svg>
<use xlink:href=/packages/lesiontracker/assets/icons.svg#icon-ui-warning></use>
</svg>
</div>
{{ /if }}
{{ #if responseStatus }}
<div>
CR
</div>
{{ /if }}
</div>
</div>
<div class="lesionDetails">
<div class='location'>
{{ #if location }}
{{location}}
{{ else }}
(No description)
{{ /if }}
</div>
<div class="timepointData">
<!--Each time point as a column-->
{{# each timepoints }}
{{> lesionTableTimepointCell}}
{{/ each }}
</div>
</div>
</div>
</template>
@@ -1,3 +1,39 @@
.lesionTableRow .location
&:hover
font-weight: bold
@import "{design}/app"
.lesionTableRow
display: flex
width: 100%
.lesionRowSidebar
flex: 1
color: $textSecondaryColor
background: $darkUiColor
max-width: 20px
height: 60px
.lesionNumber
padding: 3px
&.active
border-left: 4px $activeColor solid
.lesionDetails
flex: 1
display: flex
flex-direction: column
align-items: stretch
.location
flex: 1
text-align: center
color: $textSecondaryColor
height: 20px
width: 100%
.timepointData
display: flex
div
flex: 1
text-align: center
justify-content: space-around
@@ -1,11 +1,11 @@
<template name="lesionTableTimepointCell">
{{ #if hasDataAtThisTimepoint }}
{{ #if isBirectional }}
<td class="lesionTableTimepointCell targetCell" tabindex="1">{{displayData}}</td>
{{ else }}
<td class="lesionTableTimepointCell nonTargetCell" tabindex="1">{{displayData}}</td>
{{ /if }}
<div class="lesionTableTimepointCell" tabindex="1">
{{displayData}}
</div>
{{ else }}
<td class="lesionTableTimepointCell empty"></td>
<div class="lesionTableTimepointCell empty">
...
</div>
{{ /if }}
</template>
@@ -1,8 +1,6 @@
@import "{design}/app"
.lesionTableTimepointCell
&:hover
font-weight: bold
.nonTargetCell
&:hover
color: red
border-left: $uiBorderThickness $uiBorderColor solid
color: $textPrimaryColor
padding: 0 10px
@@ -1,6 +1,10 @@
<template name="lesionTableTimepointHeader">
<th class="lesionTableTimepointCell {{ #if isLoaded }}loaded{{/if}}" title="Latest study date: {{formatDA latestDate "MM/DD/YYYY"}}"
data-toggle="tooltip" data-placement="top">
{{ timepointName }}
</th>
<div class="lesionTableTimepointHeader">
<div class="timepointName">
{{ timepointName }}
</div>
<div class="timepointDate">
{{ formatDA latestDate "DD-MMM-YY"}}
</div>
</div>
</template>
@@ -1,3 +1,15 @@
#lesionTableContainer .lesionTable thead tr th.lesionTableTimepointCell.loaded
//font-weight: bold
color: #4fbfff
@import "{design}/app"
.lesionTableTimepointHeader
flex: 1
justify-content: space-around
div
text-align: center
.timepointName
color: $textSecondaryColor
.timepointDate
color: $textPrimaryColor
border-left: $uiBorderColor $uiBorderThickness solid
@@ -0,0 +1,35 @@
<template name="lesionTracker">
<div class="topBar">
<a class="brandSection pull-left" href="http://ohif.org">
<img class="logoImage" src="/images/logo.png">
<div class="logoText">
Open Health Imaging Foundation
</div>
</a>
{{ #unless onStudyList}}
<div class="studyListLinkSection pull-left">
<a id="loadStudyList">Study List</a>
</div>
{{ /unless }}
<div class="userAccountSection pull-right">
{{>userAccountMenu}}
</div>
</div>
{{>timeoutCountdownDialog}}
<div id="worklistTabs" class="tab-content">
<div class="tab-pane active" id="worklistTab">
<div class="worklistContainer">
{{> worklistResult }}
</div>
</div>
<div class="tab-pane" id="viewerTab">
</div>
</div>
{{ >associationModal }}
{{ >optionsModal }}
{{ >serverInformationModal }}
{{ >confirmRemoveTimepointAssociation }}
{{ >lastLoginModal }}
{{ >studyContextMenu }}
{{ >progressDialog}}
</template>
@@ -0,0 +1,65 @@
// Define the ViewerData global object
// If there is currently any Session data for this object,
// use this to repopulate the variable
ViewerData = Session.get('ViewerData') || {};
Template.lesionTracker.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);
}
});
Template.lesionTracker.onRendered(function() {
var templateData = Template.currentData();
if (templateData && templateData.studyInstanceUid) {
var studyInstanceUid = templateData.studyInstanceUid;
openNewTab(studyInstanceUid, studyInstanceUid);
} else {
// If there is a tab set as active in the Session,
// switch to that now.
var contentId = Session.get('activeContentId');
// TODO: FIx this it seems to be forcing two switches
switchToTab(contentId);
}
Meteor.subscribe('hangingprotocols');
});
Template.lesionTracker.events({
'click #tablist a[data-toggle="tab"]': function(e) {
// If this tab is already active, do nothing
var tabButton = $(e.currentTarget);
var tabTitle = tabButton.parents('.tabTitle');
if (tabTitle.hasClass('active')) {
return;
}
// Otherwise, switch to the tab
var contentId = tabButton.data('target').replace('#', '');
switchToTab(contentId);
},
'click #loadStudyList': function() {
switchToTab('worklistTab');
}
});
Session.set('defaultSignInMessage', 'Tumor tracking in your browser.');
Template.lesionTracker.helpers({
showWorklistMenu: function() {
return Template.instance().showWorklistMenu.get();
},
onStudyList: function() {
return (Session.get('activeContentId') === 'worklistTab');
}
});
@@ -0,0 +1,47 @@
@import "{design}/app"
.topBar
height: $topBarHeight
color: $textPrimaryColor
background-color: $primaryBackgroundColor
.brandSection
height: 100%
display: inline-block
img.logoImage
height: 100%
margin: 0 5px
.logoText
display: inline-block
font-family: $logoFontFamily
font-weight: $logoFontWeight
color: $textPrimaryColor
.studyListLinkSection
border-left: $uiBorderThickness solid $uiBorderColor;
padding: 5px
margin: 0 10px
a
text-decoration: none
cursor: pointer
line-height: 100%
.userAccountSection
margin: 0 10px
#worklistTabs
height: "calc(100% - %s)" % $topBarHeight
// Override Viewerbase's borders
#imageViewerViewports .viewportContainer
border: none !important
outline: 0 !important // Prevent blue outline in Chrome
&:hover
&.active
&:hover.active
border: none !important
outline: 0 !important // Prevent blue outline in Chrome
@@ -1,12 +1,3 @@
<template name="lesionTrackerLayout">
<div class="logoContainer">
{{>userAccountMenu}}
<a class="navbar-brand" href="http://ohif.org">
<img src="/images/logo.png">
<h4 class="name">Open Health Imaging Foundation</h4>
</a>
</div>
{{>timeoutCountdownDialog}}
{{>progressDialog}}
{{>yield }}
</template>
@@ -1,126 +0,0 @@
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) {
// Show modal dialog
handle = setInterval(function() {
leftTime--;
// Set countdownDialogLeftTime session
Session.set('countdownDialogLeftTime', leftTime);
// Show dialog
var dialog = $('#timeoutCountdownDialog');
if (dialog.css('display') === 'none') {
dialog.css('display', 'block');
}
}, 1000);
});
$(document).on('TriggerCloseTimeoutCountdownDialog', function(e) {
var dialog = $('#timeoutCountdownDialog');
dialog.css('display', 'none');
if (handle) {
clearInterval(handle);
// Close the dialog
dialog.css('display', 'none');
// Remove reviewers info for the user
Meteor.call('removeUserFromReviewers', Meteor.userId());
}
});
});
Template.lesionTrackerLayout.onRendered(function() {
var oldUserId = undefined;
var userName;
var lastLoginModalInterval;
Tracker.autorun(function() {
// Hook login/logout
var newUserId = Meteor.userId();
if (oldUserId === null && newUserId) {
Session.set('showLastLoginModal', true);
userName = Meteor.user().profile.fullName;
// Log
HipaaLogger.logEvent({
eventType: 'init',
userId: Meteor.userId(),
userName: userName
});
} else if (newUserId === null && oldUserId) {
// Set showLastLoginModal as null
Session.set('showLastLoginModal', null);
// Destroy interval for last login modal
Meteor.clearInterval(lastLoginModalInterval);
console.log('The user logged out');
// Log
// TODO: eventype is not defined for logout in hipaa-audit-log
/*HipaaLogger.logEvent({
eventType: 'logout',
userId: oldUserId,
userName: userName
});*/
// Remove the user from Reviewers
Meteor.call('removeUserFromReviewers', oldUserId);
}
oldUserId = Meteor.userId();
// Trigger last login date popup
if (Session.get('showLastLoginModal')) {
Modal.show('lastLoginModal');
lastLoginModalInterval = Meteor.setInterval( function() {
Modal.hide('lastLoginModal');
Session.set("showLastLoginModal", null);
}, 3000);
return true;
}
});
});
@@ -1,38 +0,0 @@
.logoContainer
position: absolute
height: 40px
top: 0
right: 0
padding: 0 10px
color: white
@media only screen and (min-device-width : 375px) and (max-device-width : 667px) and (orientation : portrait)
width: 15%
.navbar-brand
display: inline-block
padding: 0
margin-right: 5px
height: 100%
img
height: 30px
float: left
margin-right: 10px
margin-top: 5px
/* Media queries for iPhone6*/
@media only screen and (min-device-width : 375px) and (max-device-width : 667px) and (orientation : portrait)
height: 20px
margin-top: 10px
margin-bottom: 10px
margin-right: 5px
h4.name
float: left
margin: 10px 0
font-family: "Sanchez"
color: white
@media only screen and (min-device-width : 375px) and (max-device-width : 667px) and (orientation : portrait)
font-size: 0.3em
font-size: 0.3em
width: 50%
@@ -1,3 +1,102 @@
@import "{design}/app"
$viewportTagPadding = 20px
$imageSliderBorderRadius = 57px
$imageSliderColor = #22374d
$imageSliderTrackColor = rgba(0,0,0,0)
$imageSliderBorder = none
// Note that these are backwards due to the magic needed to make a cross-browser vertical slider
$imageSliderWidth = 12px
$imageSliderHeight = 39px
$imageSliderCursor = grab
.imageViewerViewportOverlay
.timepointName
color: #32BFFF
color: $textSecondaryColor
.dicomTag
font-weight: 200
.topleft
top: $viewportTagPadding
left: $viewportTagPadding
.topright
top: $viewportTagPadding
right: $viewportTagPadding
text-align: right
.bottomleft
bottom: $viewportTagPadding
left: $viewportTagPadding
.bottomright
bottom: $viewportTagPadding
right: $viewportTagPadding
text-align: right
.imageControls
#scrollbar
#imageSlider
-webkit-appearance: none
background-color: $imageSliderTrackColor
// --- Style the range track --- //
&::-webkit-slider-runnable-track
height: 5px
border: none
cursor: pointer
background-color: $imageSliderTrackColor
z-index: 6
&::-moz-range-track
height: 2px
border: none
cursor: pointer
background-color: $imageSliderTrackColor
z-index: 6
&::-ms-track
width: 100%
height: 2px
border: none
cursor: pointer
animate: 0.2s
background: transparent
border-width: 15px 0
color: $imageSliderTrackColor
// Hide any fill IE tries to add
&::-ms-fill-lower
background: $imageSliderTrackColor
&::-ms-fill-upper
background: $imageSliderTrackColor
// --- Style the range thumb --- //
&::-webkit-slider-thumb
-webkit-appearance: none !important
background-color: $imageSliderColor
border-radius: $imageSliderBorderRadius
border: $imageSliderBorder
height: $imageSliderWidth
width: $imageSliderHeight
cursor: $imageSliderCursor
margin-top: -7px
&::-moz-range-thumb
background-color: $imageSliderColor
border-radius: $imageSliderBorderRadius
border: $imageSliderBorder
height: $imageSliderWidth
width: $imageSliderHeight
cursor: $imageSliderCursor
z-index: 7
&::-ms-thumb
background-color: $imageSliderColor
border-radius: $imageSliderBorderRadius
border: $imageSliderBorder
height: $imageSliderWidth
width: $imageSliderHeight
cursor: $imageSliderCursor
@@ -45,7 +45,10 @@ Template[defaultTemplate].helpers({
return;
}
var timepointReviewers = Reviewers.findOne({timepointId: timepoint.timepointId});
var timepointReviewers = Reviewers.findOne({
timepointId: timepoint.timepointId
});
if (!timepointReviewers) {
return;
}
@@ -59,7 +62,7 @@ function getReviewerTipText(reviewers) {
return;
}
var newReviewers = reviewers.filter(function ( reviewer ) {
var newReviewers = reviewers.filter(function(reviewer) {
return reviewer.userId !== Meteor.userId();
});
@@ -76,6 +79,7 @@ function getReviewerTipText(reviewers) {
if (index > 0) {
tipText += ',';
}
tipText += reviewer.userName;
});
@@ -96,4 +100,4 @@ WorklistTabs.find().observe({
});
}
}
});
});
@@ -0,0 +1,43 @@
<template name="radialProgressBar">
<div class="radialProgress">
<svg id="svg"
width="46"
height="46"
viewPort="0 0 50 50"
version="1.1"
xmlns="http://www.w3.org/2000/svg">
<!-- stroke-dasharray is 2 * PI * radius !-->
<circle r="13"
cx="23"
cy="23"
fill="transparent"
stroke-dasharray="81.64"
stroke-dashoffset="0">
</circle>
<circle id="bar"
r="13"
cx="23"
cy="23"
fill="transparent"
stroke-dasharray="81.64"
style="stroke-dashoffset: {{progressRadius}}px;">
</circle>
</svg>
<div class="progressText">
{{ #if isLocked }}
<svg>
<use xlink:href=/packages/lesiontracker/assets/icons.svg#icon-status-lock></use>
</svg>
{{ else }}
{{ #if progressComplete }}
<svg>
<use xlink:href=/packages/lesiontracker/assets/icons.svg#icon-status-complete></use>
</svg>
{{ else }}
{{progressText}}
{{ /if }}
{{ /if }}
</div>
</div>
</template>
@@ -0,0 +1,13 @@
Template.radialProgressBar.helpers({
progressComplete() {
const instance = Template.instance();
return instance.data.progressPercent === 100;
},
progressRadius() {
var radius = 13 * 2 * Math.PI;
const instance = Template.instance();
var percentLeft = (100 - instance.data.progressPercent) / 100;
return percentLeft * radius;
}
});
@@ -0,0 +1,41 @@
@import "{design}/app"
$circleSize = 46px
$progressTextColor = #6fbde2
.radialProgress
height: $circleSize
width: $circleSize
box-shadow: 0 0 1em black
border-radius: 100%
position: relative
#svg circle
stroke-dashoffset: 0
transition: stroke-dashoffset 1s linear
stroke: #203244
stroke-width: 3px
#svg #bar
stroke: $activeColor
-ms-transform: rotate(270deg); /* IE 9 */
-ms-transform-origin: center center; /* IE 9 */
-webkit-transform: rotate(270deg); /* Chrome, Safari, Opera */
-webkit-transform-origin: center center; /* Chrome, Safari, Opera */
transform: rotate(270deg);
transform-origin: center center;
.progressText
position: absolute;
width: $circleSize;
height: $circleSize;
line-height: $circleSize;
font-family: Roboto-Bold;
font-size: 17px;
text-align: center;
color: $progressTextColor;
top: 0;
svg
width: 17px
height: 17px
@@ -1,4 +1,3 @@
function parseUrl(url) {
var parser = document.createElement('a');
parser.href = url;
@@ -11,14 +10,19 @@ Template.serverInformationModal.helpers({
var serviceInfo = Meteor.settings[defaultServiceType];
if (defaultServiceType === 'dicomWeb') {
var serverInformationDicom = [];
var endpoints = serviceInfo["endpoints"];
var endpoints = serviceInfo['endpoints'];
endpoints.forEach(function(endpoint) {
var parsedUrl = parseUrl(endpoint.qidoRoot);
serverInformationDicom.push({host: parsedUrl.hostname, port: parsedUrl.port, aeTitle: endpoint.name});
serverInformationDicom.push({
host: parsedUrl.hostname,
port: parsedUrl.port,
aeTitle: endpoint.name
});
});
return serverInformationDicom;
}
return serviceInfo;
}
});
});
@@ -0,0 +1,29 @@
<template name="studySeriesQuickSwitch">
<div class="quickSwitch {{side}}">
<div class="switchSection {{#if equals side "left"}}pull-right{{/if}} js-show-studies">
<div class="label">Study</div>
<div class="studySwitch">
</div>
</div>
<div class="switchSection {{#if equals side "left"}}pull-right{{/if}}">
<div class="label">Series</div>
<div class="seriesSwitch {{#if equals side "left"}}mirror-x{{/if}} js-show-series">
<svg viewBox="0 0 53 53">
<rect fill="#22364C" x="0" y="38" width="15" height="15" rx="3"></rect>
<rect fill="#22364C" x="19" y="38" width="15" height="15" rx="3"></rect>
<rect fill="#22364C" x="38" y="19" width="15" height="15" rx="3"></rect>
<rect fill="#22364C" x="19" y="19" width="15" height="15" rx="3"></rect>
<rect fill="#40D9B8" x="0" y="19" width="15" height="15" rx="3"></rect>
<rect fill="#22364C" x="38" y="0" width="15" height="15" rx="3"></rect>
<rect fill="#22364C" x="19" y="0" width="15" height="15" rx="3"></rect>
<rect fill="#22364C" x="0" y="0" width="15" height="15" rx="3"></rect>
</svg>
</div>
</div>
<div class="studyHover js-hide-studies {{ #unless studiesOpen}}hidden{{/unless}}">
</div>
<div class="seriesHover js-hide-series {{ #unless seriesOpen }}tiny{{/unless}}">
</div>
</div>
</template>
@@ -0,0 +1,34 @@
Template.studySeriesQuickSwitch.onCreated(function studySeriesQuickSwitchOnCreated() {
const instance = this;
instance.seriesOpen = new ReactiveVar(false);
instance.studiesOpen = new ReactiveVar(false);
});
Template.studySeriesQuickSwitch.events({
'mouseenter .js-show-series'(event, instance) {
instance.seriesOpen.set(true);
},
'mouseenter .js-show-studies'(event, instance) {
instance.studiesOpen.set(true);
},
'mouseleave .js-hide-series'(event, instance) {
instance.seriesOpen.set(false);
},
'mouseleave .js-hide-studies'(event, instance) {
instance.studiesOpen.set(false);
}
});
Template.studySeriesQuickSwitch.helpers({
seriesOpen() {
return Template.instance().seriesOpen.get();
},
studiesOpen() {
return Template.instance().studiesOpen.get();
}
});
@@ -0,0 +1,68 @@
@import "{design}/app"
$switchSize = 50px
.quickSwitch
position: absolute
width: 140px
height: 80px
top: 5px
div.switchSection
float: left
display: inline-block
margin: 0 5px
.label
display: block
text-transform: uppercase
text-align: center
font-size: 12px
font-weight: 100
color: $textSecondaryColor
.studySwitch
display: block
margin: 0 auto
width: $switchSize
height: $switchSize
border-radius: 11px
background-color: $darkUiColor
border: solid 1px $activeColor
.seriesSwitch
display: block
width: $switchSize
height: $switchSize
svg
width: $switchSize
height: $switchSize
.mirror-x
transform(scaleX(-1));
.studyHover
position: absolute
z-index: 10000
top: 70px
left: -350px
width: 400px
height: 500px
background: red
transition(all 0.3s ease)
.seriesHover
position: absolute
z-index: 10000
top: 70px
left: 50px
width: 700px
height: 900px
background: blue
transition(all 0.3s ease)
&.tiny
width: 0
height: 0
opacity: 0
@@ -0,0 +1,9 @@
<template name="studyTimepoint">
<div class="studyTimepointWrapper">
<div class="studyTimepoint">
{{#each study in (studies this.timepoint)}}
{{>studyTimepointStudy study=study}}
{{/each}}
</div>
</div>
</template>
@@ -0,0 +1,58 @@
// Initialize the timepoint wrapper max-height to enable CSS transition
Template.studyTimepoint.onRendered(() => {
const instance = Template.instance();
const $studies = instance.$('.studyTimepoint');
const $wrapper = $studies.closest('.studyTimepointWrapper');
const $timepoint = $wrapper.closest('.timepointEntry');
const studiesVisible = $studies.is(':visible');
if (!studiesVisible) {
$timepoint.addClass('active');
}
// Recalculates the timepoint height to make CSS transition smoother
$studies.trigger('displayStateChanged');
if (!studiesVisible) {
$timepoint.removeClass('active');
}
});
Template.studyTimepoint.events({
// Changes the selected study
'selectionChanged .studyTimepoint'(event, instance, selection) {
const $selection = $(selection);
const $thumbnails = $selection.find('.studyTimepointThumbnails');
const $timepoint = instance.$('.studyTimepoint');
// Set the max-height to inherit to be able to expand the wrapper on its full height
instance.$('.studyTimepointWrapper').css('max-height', 'inherit');
// Removes selected state from all studies but the triggered study
instance.$('.studyTimepointStudy').not(selection).removeClass('active');
// Toggle selected state for the triggered study
$selection.toggleClass('active');
// Recalculates the timepoint height to make CSS transition smoother
$thumbnails.one('transitionend', () => $timepoint.trigger('displayStateChanged'));
},
// It should be triggered when the timepoint height is changed
'displayStateChanged .studyTimepoint'(event, instance) {
const $timepoint = $(event.currentTarget);
const $wrapper = $timepoint.closest('.studyTimepointWrapper');
// Set the max-height for the wrapper to make CSS transition smoother
$wrapper.css('max-height', $timepoint.height());
}
});
Template.studyTimepoint.helpers({
studies: function(timepoint) {
const query = {
selected: true
};
return ViewerStudies.find(query);
}
});
@@ -0,0 +1,8 @@
@import "{design}/app"
.studyTimepointWrapper
overflow: hidden
transition($sidebarTransition)
.timepointEntry:not(.active) .studyTimepointWrapper
max-height: 0 !important
@@ -0,0 +1,29 @@
<template name="studyTimepointBrowser">
<div class="studyTimepointBrowser">
<div class="studyTimepointScrollArea">
<div class="p-x-1">
{{#each timepoint in timepoints}}
{{#if shouldShowTimepoint timepoint @index}}
<div class="timepointEntry p-y-2 active">
<div class="timepointHeader">
<div class="timepointDetails clearfix">
<div class="timepointFollowupTitle pull-left">
{{timepointTitle timepoint timepoints.count @index}}
</div>
<div class="expandIcon pull-right">
<i class="fa fa-chevron-down"></i>
</div>
<div class="timepointDate pull-right m-r-1">{{formatDA timepoint.earliestDate 'D-MMM-YYYY'}}</div>
</div>
<!-- TODO: [design] find out where to get this information -->
<div class="timepointModalities">2 MR, 2 CT</div>
</div>
{{>studyTimepoint timepoint=timepoint index=@index}}
</div>
<hr class="m-y-1">
{{/if}}
{{/each}}
</div>
</div>
</div>
</template>
@@ -0,0 +1,46 @@
Template.studyTimepointBrowser.events({
'click .timepointHeader'(event, instance) {
const $timepoint = $(event.currentTarget).closest('.timepointEntry');
// Recalculates the timepoint height to make CSS transition smoother
$timepoint.find('.studyTimepoint').trigger('displayStateChanged');
// Toggle active class to group/ungroup timepoint studies
$timepoint.toggleClass('active');
}
});
Template.studyTimepointBrowser.helpers({
timepoints: function() {
const sort = {
sort: {
earliestDate: -1
}
};
return Timepoints.find({}, sort);
},
shouldShowTimepoint(timepoint, index) {
const instance = Template.instance();
if (instance.data.timepointViewType.get() === 'all') {
return true;
}
return index < 4 || timepoint.timepointType === 'baseline';
},
// Build the timepoint title based on its date
timepointTitle(timepoint, total, index) {
if (timepoint.timepointType === 'baseline') {
return 'Baseline';
}
const states = {
0: '(Current)',
1: '(Prior)',
2: '(Nadir)'
};
// TODO: [design] find out how to define the nadir timepoint
const followUp = total - index - 1;
const parenthesis = states[index] || '';
return `Follow-up ${followUp} ${parenthesis}`;
}
});
@@ -0,0 +1,68 @@
@import "{design}/app"
.studyTimepointBrowser
background-color: $primaryBackgroundColor
float: left
height: calc(100% - 55px - 50px - 30px)
overflow: hidden
width: 100%
.timepointEntry
&.active
.timepointModalities
height: 0
.timepointDate
display: none
.expandIcon i
transform: rotateX(180deg)
.timepointFollowupTitle
font-size: 14px
color: $textSecondaryColor
text-transform: uppercase
.timepointHeader
cursor: pointer
font-size: 13px
&
.expandIcon i
.timepointFollowupTitle
.timepointDate
.timepointModalities
color: $textSecondaryColor
transition($sidebarTransition)
&:hover
&
.timepointFollowupTitle
.expandIcon i
.timepointDate
.timepointModalities
color: $textPrimaryColor !important
.timepointDetails
.timepointModalities
line-height: 25px
.timepointModalities
height: 25px
overflow: hidden
.expandIcon i
color: $defaultColor
.timepointFollowupTitle
color: $textPrimaryColor
font-family: Roboto
padding-top: 2px
.studyTimepointScrollArea
height: 100%
overflow-x: hidden
overflow-y: auto
margin-right: -16px
padding-bottom: 20px
padding-right: 16px
&::-webkit-scrollbar
display: none
@@ -0,0 +1,18 @@
<template name="studyTimepointStudy">
<div class="studyTimepointStudy">
<div class="studyModality">
<div class="studyModalityBox">
{{this.study.modalities}}
</div>
<div class="studyModalityText">
<div class="studyModalityDate">{{formatDA this.study.studyDate 'D-MMM-YYYY'}}</div>
<div class="studyModalityDescription">{{this.study.studyDescription}}</div>
</div>
</div>
<div class="studyTimepointThumbnails">
{{#each thumbnail in (thumbnails this.study)}}
{{>thumbnailEntry thumbnail}}
{{/each}}
</div>
</div>
</template>
@@ -0,0 +1,40 @@
// Initialize the study wrapper max-height to enable CSS transition
Template.studyTimepointStudy.onRendered(() => {
const instance = Template.instance();
const $study = instance.$('.studyTimepointStudy');
const $thumbnails = instance.$('.studyTimepointThumbnails');
$study.addClass('active');
$thumbnails.css('max-height', $thumbnails.height());
$study.removeClass('active');
});
Template.studyTimepointStudy.events({
// Recalculates the timepoint height to make CSS transition smoother
'transitionend .studyTimepointThumbnails'(event, instance) {
if (event.target === event.currentTarget) {
$(event.currentTarget).closest('.studyTimepoint').trigger('displayStateChanged');
}
},
// Changes the current study selection for the clicked study
'click .studyModality'(event, instance) {
const $study = $(event.currentTarget).closest('.studyTimepointStudy');
const $timepoint = $study.closest('.studyTimepoint');
const study = $study[0];
$timepoint.trigger('selectionChanged', [study]);
}
});
Template.studyTimepointStudy.helpers({
thumbnails: function(study) {
var stacks = createStacks(study);
var array = [];
stacks.forEach(function(stack, index) {
array.push({
thumbnailIndex: index,
stack: stack
});
});
return array;
}
});
@@ -0,0 +1,99 @@
@import "{design}/app"
$boxBackgroundColor = #3E5975
$boxBorderColor = transparent
$boxHoverBackgroundColor = #14191E
$boxHoverBorderColor = #2d4660
$boxActiveBackgroundColor = $activeColor
$boxActiveTextColor = $primaryBackgroundColor
$boxActiveBorderColor = #131D29
$boxWidth = 50px
$borderThickness = 2px
$boxBorderRadius = 8px
$nestingMargin = 6px
$spacerX = 7px
$spacerY = 12px
.studyTimepointStudy
&.active
.studyModality
box-shadow: inset 0 0 0 3px $activeColor
.studyModalityBox
color: $boxActiveTextColor
&
&:before
&:after
background-color: $boxActiveBackgroundColor
border-color: $boxActiveBorderColor
&:not(.active) .studyTimepointThumbnails
max-height: 0 !important
.studyTimepointThumbnails
overflow: hidden
transition($sidebarTransition)
.studyModality
border: 1px solid $boxBorderColor
border-radius: 12px
cursor: pointer
font-family: Roboto
padding: $spacerY $spacerX ($spacerY - 1)
position: relative
transition($sidebarTransition)
z-index: 1
&:hover
background-color: $boxHoverBackgroundColor
border-color: $boxHoverBorderColor
.studyModalityText
font-size: 13px
left: ($spacerX * 3) + $boxWidth + ($nestingMargin * 3)
line-height: 14px
position: absolute
right: $spacerX
top: $spacerY
.studyModalityDate
margin-top: 8px
color: $textSecondaryColor
.studyModalityDescription
margin-top: 8px
color: $textPrimaryColor
.studyModalityBox
color: $textSecondaryColor
font-size: 22px
line-height: $boxWidth
margin-left: $nestingMargin * 2
margin-top: $nestingMargin * 2
position: relative
text-align: center
text-transform: uppercase
&
&:before
&:after
background-color: $boxBackgroundColor
border: $borderThickness solid $primaryBackgroundColor
border-radius: $boxBorderRadius
height: $boxWidth + ($borderThickness * 2)
transition($sidebarTransition)
width: $boxWidth + ($borderThickness * 2)
&:before
&:after
display: block
content: ''
position: absolute
&:before
top: -($borderThickness + $nestingMargin)
left: -($borderThickness + $nestingMargin)
z-index: -1
&:after
top: -($borderThickness + ($nestingMargin * 2))
left: -($borderThickness + ($nestingMargin * 2))
z-index: -2
@@ -1,10 +1,10 @@
<template name="timeoutCountdownDialog">
<div id="timeoutCountdownDialog">
<div class="dialogHeader">
<h5>Session is about to expire!</h5>
<h5>Your session is about to expire.</h5>
</div>
<div class="dialogContent">
<p>You will be log out in {{leftTime}} {{secondsText}}.</p>
<p>You will be logged out in {{timeLeft}}.</p>
</div>
</div>
</template>
@@ -1,9 +1,39 @@
Template.timeoutCountdownDialog.helpers({
leftTime: function(e) {
return Session.get("countdownDialogLeftTime");
},
secondsText: function() {
return Session.get("countdownDialogLeftTime") > 1? "seconds": "second";
timeLeft: function() {
var timeLeft = Session.get('countdownDialogLeftTime');
var suffix = timeLeft > 1 ? 'seconds': 'second';
return timeLeft + suffix;
}
});
});
Template.timeoutCountdownDialog.onRendered(function() {
// Show countdown dialog
var timeoutCountdownInterval;
$(document).on('TriggerOpenTimeoutCountdownDialog', function(e, leftTime) {
// Show modal dialog
timeoutCountdownInterval = setInterval(function() {
// Set countdownDialogLeftTime session
Session.set('countdownDialogLeftTime', --leftTime);
var dialog = $('#timeoutCountdownDialog');
// Show dialog
dialog.css('display', 'block');
}, 1000);
});
$(document).on('TriggerCloseTimeoutCountdownDialog', function() {
var dialog = $('#timeoutCountdownDialog');
// Close the dialog
dialog.css('display', 'none');
if (timeoutCountdownInterval) {
clearInterval(timeoutCountdownInterval);
// Remove reviewer info for the user
Meteor.call('removeUserFromReviewers', Meteor.userId());
}
});
});
@@ -0,0 +1,79 @@
<template name="toolbarSection">
<div class="toolbarSection">
<div id="studySidebarToggle" class="pull-left toolbarSectionButton">
<div class="capsule svgContainer">
<svg>
<use xlink:href="/packages/lesiontracker/assets/icons.svg#icon-studies"></use>
</svg>
</div>
<div class="buttonLabel">
Studies
</div>
</div>
<div class="pull-left">
{{ #each toolbarButtons }}
{{ >toolbarSectionButton }}
{{ /each }}
<div id="moreTools" class="toolbarSectionButton">
<div class="svgContainer">
<svg>
<use xlink:href="/packages/lesiontracker/assets/icons.svg#icon-tools-more"></use>
</svg>
</div>
<div class="buttonLabel">
More <i class="fa fa-caret-down" aria-hidden="true"></i>
</div>
</div>
</div>
{{ >studySeriesQuickSwitch side="left"}}
{{ >studySeriesQuickSwitch side="right"}}
<div class="pull-right toolbarSectionEntry">
{{ >caseProgress }}
</div>
<div class="capsule-group pull-right">
<div id="lesionSidebarToggle" class="capsule-entry">
<div class="svgContainer capsule">
<svg>
<use xlink:href="/packages/lesiontracker/assets/icons.svg#icon-measurements-lesions"></use>
</svg>
</div>
<div class="buttonLabel">
Lesions
</div>
</div>
<div id="additionalMeasurementsSidebarToggle" class="capsule-entry">
<div class="svgContainer capsule">
<svg>
<use xlink:href="/packages/lesiontracker/assets/icons.svg#icon-measurements-additional"></use>
</svg>
</div>
<div class="buttonLabel">
Additional
</div>
</div>
</div>
<div class="pull-right toolbarSectionButton">
<div class="svgContainer">
<svg>
<use xlink:href="/packages/lesiontracker/assets/icons.svg#icon-hud"></use>
</svg>
</div>
<div class="buttonLabel">
HUD
</div>
</div>
<div class="pull-right toolbarSectionButton">
<div class="svgContainer">
<svg>
<use xlink:href="/packages/lesiontracker/assets/icons.svg#icon-trial-info"></use>
</svg>
</div>
<div class="buttonLabel">
Trial
</div>
</div>
</div>
</template>
@@ -0,0 +1,133 @@
Template.toolbarSection.onRendered(function() {
var instance = this;
instance.state = instance.data.state;
});
Template.toolbarSection.helpers({
toolbarButtons: function() {
var buttonData = [];
buttonData.push({
id: 'zoom',
title: 'Zoom',
classes: 'imageViewerTool',
svgLink: '/packages/lesiontracker/assets/icons.svg#icon-tools-zoom'
});
buttonData.push({
id: 'wwwc',
title: 'Levels',
classes: 'imageViewerTool',
svgLink: '/packages/lesiontracker/assets/icons.svg#icon-tools-levels'
});
buttonData.push({
id: 'pan',
title: 'Pan',
classes: 'imageViewerTool',
svgLink: '/packages/lesiontracker/assets/icons.svg#icon-tools-pan'
});
buttonData.push({
id: 'link',
title: 'Link',
classes: 'imageViewerCommand toolbarSectionButton',
svgLink: '/packages/lesiontracker/assets/icons.svg#icon-tools-link'
});
buttonData.push({
id: 'bidirectional',
title: 'Target',
classes: 'imageViewerTool',
svgLink: '/packages/lesiontracker/assets/icons.svg#icon-tools-measure-target'
});
buttonData.push({
id: 'nonTarget',
title: 'Non-Target',
classes: 'imageViewerTool toolbarSectionButton',
svgLink: '/packages/lesiontracker/assets/icons.svg#icon-tools-measure-non-target'
});
buttonData.push({
id: 'length',
title: 'Temp',
classes: 'imageViewerTool toolbarSectionButton',
svgLink: '/packages/lesiontracker/assets/icons.svg#icon-tools-measure-temp'
});
return buttonData;
}
});
Template.toolbarSection.events({
'click #studySidebarToggle': function(event, instance) {
var isOpen = instance.data.state.get('studySidebarOpen');
instance.data.state.set('studySidebarOpen', !isOpen);
},
'click #lesionSidebarToggle': function(event, instance) {
var isOpen = instance.data.state.get('lesionSidebarOpen');
instance.data.state.set('lesionSidebarOpen', !isOpen);
instance.data.state.set('additionalMeasurementsSidebarOpen', false);
},
'click #additionalMeasurementsSidebarToggle': function(event, instance) {
var isOpen = instance.data.state.get('additionalMeasurementsSidebarOpen');
instance.data.state.set('additionalMeasurementsSidebarOpen', !isOpen);
instance.data.state.set('lesionSidebarOpen', false);
},
// TODO: Inherit these from toolbar template somehow
'click .imageViewerTool': function(e) {
$(e.currentTarget).tooltip('hide');
var tool = e.currentTarget.id;
var elements = $('.imageViewerViewport');
var activeTool = toolManager.getActiveTool();
$('.toolbarSectionButton').removeClass('active');
if (tool === activeTool) {
var defaultTool = toolManager.getDefaultTool();
console.log('Setting active tool to: ' + defaultTool);
toolManager.setActiveTool(defaultTool, elements);
} else {
console.log('Setting active tool to: ' + tool);
toolManager.setActiveTool(tool, elements);
}
},
'click .imageViewerCommand': function(e) {
$(e.currentTarget).tooltip('hide');
var command = e.currentTarget.id;
if (!OHIF.viewer.functionList.hasOwnProperty(command)) {
return;
}
var activeViewport = Session.get('activeViewport');
var element = $('.imageViewerViewport').get(activeViewport);
OHIF.viewer.functionList[command](element);
}
});
Template.toolbarSection.onRendered(function() {
var tooltipButtons = $('[data-toggle="tooltip"]');
tooltipButtons.tooltip(OHIF.viewer.tooltipConfig);
// Enable tooltips for the layout button
var extraTooltipButtons = $('[rel="tooltip"]');
extraTooltipButtons.tooltip(OHIF.viewer.tooltipConfig);
// Set disabled/enabled tool buttons that are set in toolManager
var states = toolManager.getToolDefaultStates();
var disabledToolButtons = states.disabledToolButtons;
var allToolbarButtons = $('#toolbar').find('button');
if (disabledToolButtons && disabledToolButtons.length > 0) {
for (var i = 0; i < allToolbarButtons.length; i++) {
var toolbarButton = allToolbarButtons[i];
$(toolbarButton).prop('disabled', false);
var index = disabledToolButtons.indexOf($(toolbarButton).attr('id'));
if (index !== -1) {
$(toolbarButton).prop('disabled', true);
}
}
}
});
@@ -0,0 +1,151 @@
@import "{design}/app"
.toolbarSection
height: $toolbarHeight
width: 100%
border-bottom: $uiBorderColor $uiBorderThickness solid
.capsule-group
display: inline-block
div.capsule-entry
display: inline-block
color: $defaultColor
fill: $defaultColor
stroke: $defaultColor
height: $toolbarHeight
min-width: 30px
cursor: pointer
text-align: center
margin: 0
div.capsule
width: 66px
height: 26px
&:first-child
.capsule
border-radius: 57px
background-color: #16202b
border: solid 1px #3c5d80
border-right: none
border-top-right-radius: 0
border-bottom-right-radius: 0
&:last-child
.capsule
border-radius: 57px
background-color: #16202b
border: solid 1px #3c5d80
border-left: none
border-top-left-radius: 0
border-bottom-left-radius: 0
&.disabled
opacity: 0.5
cursor: not-allowed
.svgContainer
margin: 0 auto
text-align: center
.capsule
width: 62px
height: 26px
border-radius: 100px
background-color: #16202b
border: solid 1px #3c5d80
svg
background-color: transparent
margin: 5px
width: 17px
height: 17px
&:active, &.active
color: $activeColor
svg
fill: $activeColor
stroke: $activeColor
&:hover
color: $hoverColor
svg
fill: $hoverColor
stroke: $hoverColor
.toolbarSectionButton
display: inline-block
color: $defaultColor
fill: $defaultColor
stroke: $defaultColor
padding: 0 10px
height: $toolbarHeight
min-width: 30px
cursor: pointer
text-align: center
&.disabled
opacity: 0.5
cursor: not-allowed
.buttonLabel
font-size: 12px
.svgContainer
margin: 0 auto
text-align: center
.capsule
width: 62px
height: 26px
border-radius: 100px
background-color: #16202b
border: solid 1px #3c5d80
svg
background-color: transparent
margin: 2px
width: 21px
height: 21px
&:active, &.active
color: $activeColor
svg
fill: $activeColor
stroke: $activeColor
&:hover
color: $hoverColor
svg
fill: $hoverColor
stroke: $hoverColor
.toolbarSectionEntry
display: inline-block
color: $defaultColor
fill: $defaultColor
stroke: $defaultColor
padding: 0 10px
height: $toolbarHeight
min-width: 30px
cursor: pointer
text-align: center
.quickSwitch
position: absolute
&.middle
left: calc(50% - 60px)
&.left
left: calc(50% - 160px)
&.right
left: 50%
margin-left: 20px
@@ -0,0 +1,12 @@
<template name="toolbarSectionButton">
<div id="{{id}}" class="toolbarSectionButton {{classes}}" title="{{title}}">
<div class="svgContainer">
<svg>
<use xlink:href={{svgLink}}></use>
</svg>
</div>
<div class="buttonLabel">
{{title}}
</div>
</div>
</template>
@@ -1,38 +1,46 @@
<template name="userAccountMenu">
{{#if currentUser}}
<div id="userAccountMenu" class="row-fluid">
<div class="list-group panel">
<a href="#userMenu" class="list-group-item" id="userName" data-toggle="collapse" data-parent="#userAccountMenu">{{fullName}} {{currentPath}}<span class="caret"></span></a>
<div class="collapse pos-absolute" id="userMenu">
{{#if showWorklistMenu}}
<a href="worklist" id="worklist" data-toggle="collapse" data-target="#userMenu" class="list-group-item sub-item">
<div id="userAccountMenu" class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{name}} {{currentPath}} | <i class="fa fa-cog fa-lg"></i>
<span class="caret"></span>
</a>
<ul class="dropdown-menu dropdown-menu-right">
{{#if showWorklistMenu}}
<li>
<a href="worklist" id="worklist">
<i class="fa fa-list-ul"></i> Worklist
</a>
{{else}}
<!--TODO: Move this elsewhere in the Worklist? -->
<span data-toggle="collapse" data-target="#userMenu" class="list-group-item sub-item externalItem">{{>optionsButton }}</span>
<a href="audit" id="audit" data-toggle="collapse" data-target="#userMenu" class="list-group-item sub-item">
</li>
{{else}}
<!--TODO: Move this elsewhere in the Worklist? -->
<li>
{{>optionsButton }}
</li>
<li>
<a href="/audit" id="audit">
<i class="fa fa-list-ul"></i> View Audit Log
</a>
{{/if}}
<a href="#itemSettings" class="list-group-item sub-item" data-toggle="collapse" data-parent="#itemSettings"><i class="fa fa-cog fa-lg"></i> Settings <span class="caret"></span></a>
<div class="collapse list-group-submenu" id="itemSettings">
<a href="#" id="serverInformation" class="list-group-item sub-sub-item" data-parent="#itemSettings" data-toggle="collapse" data-target="#itemSettings,#userMenu"><i class="fa fa-server fa-lg"></i> Server Information</a>
</div>
<a href="#itemProfile" class="list-group-item sub-item" data-toggle="collapse" data-parent="#itemProfile" ><i class="fa fa-user fa-lg"></i> Profile <span class="caret"></span></a>
<div class="collapse list-group-submenu" id="itemProfile">
<a href="changePassword" id="changePassword" class="list-group-item sub-sub-item" data-parent="#itemProfile" data-toggle="collapse" data-target="#itemProfile,#userMenu"><i class="fa fa-lock"></i> Change Password</a>
</div>
<a href="#" id="logoutButton" data-toggle="collapse" data-target="#userMenu" class="list-group-item sub-item">
</li>
{{/if}}
<li role="separator" class="divider"></li>
<li>
<a id="serverInformation">
<i class="fa fa-server fa-lg"></i> Server Information
</a>
</li>
<li role="separator" class="divider"></li>
<li>
<a href="changePassword" id="changePassword">
<i class="fa fa-lock"></i> Change Password
</a>
</li>
<li>
<a id="logoutButton">
<i class="fa fa-power-off"></i> Logout
</a>
</div>
</div>
</li>
</ul>
</div>
{{/if}}
</template>
@@ -1,12 +1,15 @@
Template.userAccountMenu.helpers({
fullName: function() {
return Meteor.user().profile.fullName;
name: function() {
var nameSplit = Meteor.user().profile.fullName.split(' ');
var lastName = nameSplit[nameSplit.length - 1];
nameSplit[nameSplit.length - 1] = lastName.substr(0, 1) + '.';
return nameSplit.join(' ');
},
currentUser: function() {
var verifyEmail = Meteor.settings && Meteor.settings.public && Meteor.settings.public.verifyEmail || false;
if (!Meteor.user() || !Meteor.userId()) {
return false;
return;
}
if (!verifyEmail) {
@@ -17,18 +20,13 @@ Template.userAccountMenu.helpers({
return true;
}
if (Meteor.user().emails[0].verified) {
return true;
}
return false;
return !!Meteor.user().emails[0].verified;
},
showWorklistMenu: function() {
var currentPath = Router.current().route.path(this);
if (currentPath === '/' || currentPath === '/worklist') {
return false;
if (currentPath !== '/' && currentPath !== '/worklist') {
return true;
}
return true;
}
});
@@ -39,8 +37,62 @@ Template.userAccountMenu.events({
'click #logoutButton': function() {
// Remove reviewers info for the user
Meteor.call('removeUserFromReviewers', Meteor.userId());
Meteor.logout(function() {
Router.go('/entrySignIn');
});
}
});
Template.userAccountMenu.onRendered(function() {
var oldUserId;
var lastLoginModalInterval;
this.autorun(function() {
// Hook login/logout
var user = Meteor.user();
var newUserId = user._id;
if (oldUserId === null && newUserId) {
Session.set('showLastLoginModal', true);
// Log
HipaaLogger.logEvent({
eventType: 'init',
userId: user._id,
userName: user.fullName
});
} else if (newUserId === null && oldUserId) {
// Set showLastLoginModal as null
Session.set('showLastLoginModal', null);
// Destroy interval for last login modal
Meteor.clearInterval(lastLoginModalInterval);
console.log('The user logged out');
// Log
// TODO: eventType is not defined for logout in hipaa-audit-log
/*HipaaLogger.logEvent({
eventType: 'logout',
userId: oldUserId,
userName: userName
});*/
// Remove the user from Reviewers
Meteor.call('removeUserFromReviewers', oldUserId);
}
oldUserId = Meteor.userId();
// Trigger last login date popup
if (Session.get('showLastLoginModal')) {
Modal.show('lastLoginModal');
lastLoginModalInterval = Meteor.setInterval(function() {
Modal.hide('lastLoginModal');
Session.set('showLastLoginModal', null);
}, 3000);
}
});
});
@@ -1,34 +1,15 @@
@import "{design}/app"
#userAccountMenu
position: relative
float: right
padding: 5px
height: 100%
line-height: 40px
z-index: 999
.list-group
background-color: transparent
color: white
line-height: 35px
margin-left: 10px
.list-group-item
padding: 5px 10px
width: 200px
a#userName
padding: 0
background-color: transparent
color: white
border: solid 1px transparent
text-align: right
display: inline-block
width: auto
#userMenu
position: absolute
right: 0
.externalItem
a
color: #555
.dropdown-toggle
cursor: pointer
text-decoration: none
background-color: $primaryBackgroundColor
color: $textSecondaryColor
.list-group-submenu
.sub-sub-item
padding-left: 20px
padding-right: 20px
ul.dropdown-menu
li
cursor: pointer
@@ -0,0 +1,16 @@
<template name="viewer">
{{#if Template.subscriptionsReady}}
<div id="viewer">
{{>confirmDeleteDialog}}
{{>lesionLocationDialog}}
{{>nonTargetLesionDialog}}
{{>nonTargetResponseDialog}}
<!-- >conformanceCheckFeedback -->
{{ >toolbarSection }}
{{ >flexboxLayout }}
</div>
{{else}}
{{>loadingText}}
{{/if}}
</template>
@@ -0,0 +1,309 @@
Session.setDefault('activeViewport', false);
Session.setDefault('studySidebarOpen', false);
Session.setDefault('lesionSidebarOpen', false);
Session.setDefault('additionalMeasurementsSidebarOpen', false);
Template.viewer.onCreated(function() {
// Attach the Window resize listener
$(window).on('resize', handleResize);
ValidationErrors.remove({});
var instance = this;
instance.data.state = new ReactiveDict();
instance.data.state.set('studySidebarOpen', Session.get('studySidebarOpen'));
instance.data.state.set('lesionSidebarOpen', Session.get('lesionSidebarOpen'));
instance.data.state.set('additionalMeasurementsSidebarOpen', Session.get('additionalMeasurementsSidebarOpen'));
var contentId = this.data.contentId;
OHIF = OHIF || window.OHIF || {
viewer: {}
};
OHIF.viewer.loadIndicatorDelay = 3000;
OHIF.viewer.defaultTool = 'wwwc';
OHIF.viewer.refLinesEnabled = true;
OHIF.viewer.isPlaying = {};
OHIF.viewer.cine = {
framesPerSecond: 24,
loop: true
};
OHIF.viewer.functionList = {
invert: function(element) {
var viewport = cornerstone.getViewport(element);
viewport.invert = !viewport.invert;
cornerstone.setViewport(element, viewport);
},
resetViewport: function(element) {
cornerstone.reset(element);
},
playClip: function(element) {
var viewportIndex = $('.imageViewerViewport').index(element);
var isPlaying = OHIF.viewer.isPlaying[viewportIndex] || false;
if (isPlaying === true) {
cornerstoneTools.stopClip(element);
} else {
cornerstoneTools.playClip(element);
}
OHIF.viewer.isPlaying[viewportIndex] = !OHIF.viewer.isPlaying[viewportIndex];
Session.set('UpdateCINE', Random.id());
},
toggleLesionTrackerTools: toggleLesionTrackerTools,
clearTools: clearTools,
bidirectional: function() {
// Used for hotkeys
toolManager.setActiveTool('bidirectional');
},
nonTarget: function() {
// Used for hotkeys
toolManager.setActiveTool('nonTarget');
}
};
// The hotkey can also be an array (e.g. ["NUMPAD0", "0"])
OHIF.viewer.defaultHotkeys = OHIF.viewer.defaultHotkeys || {};
OHIF.viewer.defaultHotkeys.toggleLesionTrackerTools = 'O';
OHIF.viewer.defaultHotkeys.bidirectional = 'T'; // Target
OHIF.viewer.defaultHotkeys.nonTarget = 'N'; // Non-target
if (isTouchDevice()) {
OHIF.viewer.tooltipConfig = {
trigger: 'manual'
};
} else {
OHIF.viewer.tooltipConfig = {
trigger: 'hover'
};
}
OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer('CornerstoneNewImage', cornerstoneTools.updateImageSynchronizer);
if (ViewerData[contentId].loadedSeriesData) {
log.info('Reloading previous loadedSeriesData');
OHIF.viewer.loadedSeriesData = ViewerData[contentId].loadedSeriesData;
} else {
log.info('Setting default ViewerData');
OHIF.viewer.loadedSeriesData = {};
ViewerData[contentId].loadedSeriesData = OHIF.viewer.loadedSeriesData;
// Update the viewer data object
if (!this.data.timepointIds || this.data.timepointIds.length <= 1) {
// Update the viewer data object
ViewerData[contentId].viewportColumns = 1;
ViewerData[contentId].viewportRows = 1;
} else if (this.data.timepointIds.length > 1) {
ViewerData[contentId].viewportColumns = 2;
ViewerData[contentId].viewportRows = 1;
}
ViewerData[contentId].activeViewport = 0;
Session.set('ViewerData', ViewerData);
}
Session.set('activeViewport', ViewerData[contentId].activeViewport || false);
// Set lesion tool buttons as disabled if pixel spacing is not available for active element
instance.autorun(pixelSpacingAutorunCheck);
// Update the ViewerStudies collection with the loaded studies
ViewerStudies.remove({});
this.data.studies.forEach(function(study) {
study.selected = true;
ViewerStudies.insert(study);
});
var patientId = this.data.studies[0].patientId;
Session.set('patientId', patientId);
instance.autorun(function() {
var dataContext = Template.currentData();
instance.subscribe('singlePatientAssociatedStudies', dataContext.studies[0].patientId);
instance.subscribe('singlePatientTimepoints', dataContext.studies[0].patientId);
instance.subscribe('singlePatientMeasurements', dataContext.studies[0].patientId);
instance.subscribe('singlePatientImageMeasurements', dataContext.studies[0].patientId);
var subscriptionsReady = instance.subscriptionsReady();
log.info('autorun viewer.js. Ready: ' + subscriptionsReady);
if (subscriptionsReady) {
// Set buttons as enabled/disabled when Timepoints collection is ready
timepointAutoCheck(dataContext);
TrialResponseCriteria.validateAllDelayed();
ViewerStudies.find().observe({
added: function(study) {
// Find the relevant timepoint given the newly added study
var timepoint = Timepoints.findOne({
studyInstanceUids: {
$in: [study.studyInstanceUid]
}
});
if (!timepoint) {
log.warn('Study added to Viewer has not been associated!');
return;
}
// Update the added document with its related timepointId
ViewerStudies.update(study._id, {
$set: {
timepointId: timepoint.timepointId
}
});
}
});
ImageMeasurements.find().observe({
added: function(data) {
if (data.clientId === ClientId) {
return;
}
syncImageMeasurementAndToolData(data);
// Update each displayed viewport
updateAllViewports();
},
changed: function(data) {
if (data.clientId === ClientId) {
return;
}
syncImageMeasurementAndToolData(data);
// Update each displayed viewport
updateAllViewports();
},
removed: function(data) {
if (data.clientId === ClientId) {
return;
}
removeToolDataWithMeasurementId(data.imageId, data.toolType, data.id);
// Update each displayed viewport
updateAllViewports();
}
});
Measurements.find().observe({
added: function(data) {
if (data.clientId === ClientId) {
TrialResponseCriteria.validateAllDelayed();
return;
}
log.info('Measurement added');
// This is used to re-add tools from the database into the
// Cornerstone ToolData structure
syncMeasurementAndToolData(data);
// Update each displayed viewport
updateAllViewports();
},
changed: function(data) {
if (data.clientId === ClientId) {
TrialResponseCriteria.validateAllDelayed();
return;
}
log.info('Measurement changed');
// This is used to update changed tools from the database
// in the Cornerstone ToolData structure
syncMeasurementAndToolData(data);
// Update each displayed viewport
updateAllViewports();
TrialResponseCriteria.validateAllDelayed();
},
removed: function(data) {
log.info('Measurement removed');
// Check that this Measurement actually contains timepoint data
if (!data || !data.timepoints) {
return;
}
// Get the Measurement ID and relevant tool so we can remove
// tool data for this Measurement
var measurementId = data._id;
var toolType = data.toolType;
// Remove the measurement from all the imageIds on which it exists
// as toolData
Object.keys(data.timepoints).forEach(function(timepointId) {
// Clear the toolData for this timepoint
var imageId = data.timepoints[timepointId].imageId;
removeToolDataWithMeasurementId(imageId, toolType, measurementId);
// Set reviewer for this timepoint
if (data.timepoints[timepointId].studyInstanceUid) {
Meteor.call('setReviewer', data.timepoints[timepointId].studyInstanceUid);
}
});
// Sync database data with toolData for all the measurements
// that have just been updated
// Note that here we need to use greater than and equals to
// find the Measurements, whereas on the server it's
// only "greater than", since inside this callback the
// Measurements have already been decremented.
var measurements = Measurements.find({
patientId: data.patientId,
lesionNumberAbsolute: {
$gte: data.lesionNumberAbsolute
}
});
measurements.forEach(function(measurement) {
syncMeasurementAndToolData(measurement);
});
// Update each displayed viewport
updateAllViewports();
ValidationErrors.remove({
measurementId: data._id
});
TrialResponseCriteria.validateAll();
}
});
}
});
});
Template.viewer.onRendered(function() {
// Enable hotkeys
enableHotkeys();
});
Template.viewer.onDestroyed(function() {
// Remove the Window resize listener
$(window).off('resize', handleResize);
OHIF.viewer.updateImageSynchronizer.destroy();
});
Template.viewer.events({
'CornerstoneToolsMeasurementAdded .imageViewerViewport': function(e, template, eventData) {
handleMeasurementAdded(e, eventData);
},
'CornerstoneToolsMeasurementModified .imageViewerViewport': function(e, template, eventData) {
handleMeasurementModified(e, eventData);
},
'CornerstoneToolsMeasurementRemoved .imageViewerViewport': function(e, template, eventData) {
handleMeasurementRemoved(e, eventData);
}
});
@@ -0,0 +1,35 @@
@import "{design}/app"
#viewer
width: 100%
height: 100%
margin: 0
padding: 0
background-color: black
//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-family: "Helvetica Neue", Helvetica, Arial, sans-serif
#btnCollapse
background-color: #333
border-color: black
&.active
border-color: #333
.btnCollapseIcon
color: white
@@ -0,0 +1,6 @@
<template name="viewerMain">
<div class="viewerMain">
<div id='layoutManagerTarget'>
</div>
</div>
</template>
@@ -0,0 +1,10 @@
Template.viewerMain.onRendered(function() {
var instance = this;
var studies = instance.data.studies;
var parentElement = instance.$("#layoutManagerTarget").get(0);
window.layoutManager = new LayoutManager(parentElement, studies);
ProtocolEngine = new HP.ProtocolEngine(layoutManager, studies);
HP.setEngine(ProtocolEngine);
});
@@ -0,0 +1,17 @@
@import "{design}/app"
.viewerMain
width: 100%
height: 100%
#layoutManagerTarget
width: 100%
height: 100%
transition(all 0.3s ease)
#imageViewerViewports
.viewportContainer:not(:last-child)
.removable
.imageViewerViewport
border-right: $uiBorderThickness solid #436270