Updates for Timepoint assocation (LT-60) and Conformance Checks (LT-92, LT-93, LT-94)

This commit is contained in:
Erik Ziegler committed 2016-02-08 19:59:39 +01:00
1 parent 6c4eae3699
commit beaa56c0b3
144 files changed
+5050 -2416

No files matched your search

@@ -0,0 +1,21 @@
<template name="studyContextMenu">
<div id="studyContextMenu" class="studyContextMenu noselect"
oncontextmenu='return false;'
unselectable='on'
onselectstart='return false;'>
<ul>
<li>
<a class="disabled"><span class="fa-stack fa-lg">
<i class="fa fa-user fa-stack-1x"></i>
<i class="fa fa-ban fa-stack-2x text-danger"></i>
</span> Anonymize
</a>
<a class="disabled"><i class="fa fa-trash fa-lg"></i> Delete</a>
<a class="disabled"><i class="fa fa-send-o fa-lg"></i> Send</a>
<a class="disabled"><i class="fa fa-exchange fa-lg"></i> Export</a>
<a class="disabled"><i class="fa fa-download fa-lg"></i> Download</a>
<a class="disabled"><i class="fa fa-photo fa-lg"></i> View Series Details</a>
</li>
</ul>
</div>
</template>
@@ -0,0 +1,71 @@
function closeHandler(dialog) {
// Hide the dialog
$(dialog).css('display', 'none');
// Remove the backdrop
$('.removableBackdrop').remove();
}
/**
* This function is used inside the Worklist package to define a right click callback
*
* @param e
*/
openStudyContextMenu = function(e) {
Template.studyContextMenu.study = $(e.currentTarget);
var dialog = $('#studyContextMenu');
// Show the nonTargetLesion dialog above
var dialogProperty = {
display: 'block'
};
// Device is touch device or not
// If device is touch device, set position center of screen vertically and horizontally// jscs:ignore
if (isTouchDevice()) {
// add dialogMobile class to provide a black, transparent background
dialog.addClass('dialogMobile');
dialogProperty.top = 0;
dialogProperty.left = 0;
dialogProperty.right = 0;
dialogProperty.bottom = 0;
} else {
dialogProperty.top = e.pageY;
dialogProperty.left = e.pageX;
var pageHeight = $(document.body).height();
dialogProperty.top = Math.max(dialogProperty.top, 0);
dialogProperty.top = Math.min(dialogProperty.top, pageHeight - dialog.outerHeight());
var pageWidth = $(document.body).width();
dialogProperty.left = Math.max(dialogProperty.left, 0);
dialogProperty.left = Math.min(dialogProperty.left, pageWidth - dialog.outerWidth());
}
dialog.css(dialogProperty);
dialog.focus();
// 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.studyContextMenu.events({
'click a': function(e) {
var study = Template.studyContextMenu.study;
var id = $(e.currentTarget).attr('id');
var fn = Worklist.functions[id];
if (fn && typeof(fn) === 'function') {
fn(study);
}
var dialog = $('#studyContextMenu');
closeHandler(dialog);
}
});
@@ -0,0 +1,56 @@
.studyContextMenu
z-index: 10000
display: none
position: absolute
color: whitesmoke
background-color: #2d2d2d
width: -moz-fit-content
width: -webkit-fit-content
width: fit-content
height: -moz-fit-content
height: -webkit-fit-content
height: fit-content
min-width: 100px
font-size: 14px
text-align: left
-webkit-background-clip: padding-box
background-clip: padding-box
border: 1px solid rgba(0, 0, 0, .15)
border-radius: 4px
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175)
box-shadow: 0 6px 12px rgba(0, 0, 0, .175)
ul
width: 100%
margin: 5px 0
list-style: none
padding: 0
li
clear: both
white-space: nowrap
a
padding: 3px 15px
display: block
text-decoration: none
cursor: pointer
line-height: 25px
color: #808080
&:not(.disabled):hover
color: #439193
text-decoration: none
background-color: #1d1d1d
&.disabled
opacity: 0.4
pointer-events: none
cursor: default
.fa-lg
width: 40px
text-align: center
@@ -0,0 +1,5 @@
<template name="tabContent">
<div role="tabpanel" class="tab-pane {{active}}" id="{{contentid}}">
{{ >loadingText }}
</div>
</template>
@@ -0,0 +1,4 @@
.tab-pane
.loadingTextDiv
h5
color: #777
@@ -0,0 +1,8 @@
<template name="tabTitle">
<li role="presentation" class="tabTitle">
<a data-target="#{{contentid}}" role="tab" data-toggle="tab">
{{title}}
<button class="close">&times;</button>
</a>
</li>
</template>
@@ -0,0 +1,48 @@
Template.tabTitle.events({
/**
* Closes a tab when the close button is pressed in the title
* The next tab to the left is loaded if the current tab is closed
*
* @param e The click event used to close the tab
*/
'click .close': function(e) {
// Identify the tab title DOM node
var tab = $(e.currentTarget).parents('a[data-toggle="tab"]').eq(0);
// Get the relevent contentId that this tab title represents
// Replace any hash marks (#) that were required by Bootstrap's tab switching
var contentId = tab.data('target').replace('#', '');
// Check if we are closing the active tab. If we are, prepare to switch
// to the next tab to the left.
var activeContentId = Session.get('activeContentId');
if (activeContentId === contentId) {
// Find the index of the tab that is being closed
var tabIndex = tab.parent('li').index();
// Find the index the tab to its left
var newActiveTabIndex = Math.max(tabIndex - 1, 0);
// Find the DOM node of the tab that will be activated
var newActiveTab = $('.tabTitle').eq(newActiveTabIndex);
// Find the content ID of the tab that will be switched to
var newActiveTabLink = newActiveTab.find('a[data-toggle=tab]');
var newContentId = newActiveTabLink.data('target').replace('#', '');
// Switch to this tab
switchToTab(newContentId);
}
// Find the tab to be closed in the Tabs collection
var tabObject = WorklistTabs.findOne({
contentid: contentId
});
// Remove this tab from the Tabs collection so it is no longer rendered
WorklistTabs.remove(tabObject._id);
// Remove any stored data related to this tab from the global ViewerData structure
delete ViewerData[contentId];
}
});
@@ -0,0 +1,4 @@
.tabTitle
button.close
// Prevent blue glow around button in Chrome
outline: none
@@ -0,0 +1,24 @@
<template name="worklist">
<ul class="nav noselect" role="tablist" id="tablist">
<li role="presentation" class="tabTitle active">
<a data-target="#worklistTab" role="tab" data-toggle="tab">Study List</a>
</li>
{{ #each worklistTabs }}
{{ >tabTitle }}
{{ /each }}
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="worklistTab">
<div id="worklistContainer" class="container">
{{> worklistResult }}
</div>
</div>
{{ #each worklistTabs }}
{{ >tabContent }}
{{ /each }}
</div>
{{#each additionalTemplates}}
{{> UI.dynamic template=this}}
{{/each}}
{{ >studyContextMenu }}
</template>
@@ -0,0 +1,59 @@
/**
* Template: Worklist
*
* This is the main component of the Worklist package
*/
// 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') || {};
// Create the WorklistTabs collection
WorklistTabs = new Meteor.Collection(null);
// Define the WorklistStudies Collection
// This is a client-side only Collection which
// Stores the list of studies in the Worklist
WorklistStudies = new Meteor.Collection(null);
Template.worklist.onCreated(function() {
var self = this;
if (Worklist.subscriptions) {
Worklist.subscriptions.forEach(function(collectionName) {
self.subscribe(collectionName);
});
}
});
Template.worklist.onRendered(function() {
// If there is a tab set as active in the Session,
// switch to that now.
var contentId = Session.get('activeContentId');
switchToTab(contentId);
});
Template.worklist.helpers({
/**
* Returns the current set of Worklist Tabs
* @returns Meteor.Collection The current state of the WorklistTabs Collection
*/
worklistTabs: function() {
return WorklistTabs.find();
}
});
Template.worklist.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);
}
});
@@ -0,0 +1,105 @@
body
background-color: #202020
.noselect
-webkit-touch-callout: none
-webkit-user-select: none
-khtml-user-select: none
-moz-user-select: none
-ms-user-select: none
user-select: none
input.worklist-search
height: 25px
background-color: #888888
width: 100%
#tblStudyList
tr
height: 20px
.patient-name-input
width: 100%
.worklist-input
width: 100%
.modality-input
width: 100%
.patientid-input
width: 100%
.study-description-input
width: 100%
#worklistTab
background-color: #202020
#worklistContainer
background-color: #202020
color: white
width:90%
padding-top: 10px
#tabs
width: 100%
#tablist
border-bottom: 1px solid #424242
background-color: #202020
height: 41px
a:hover
background-color: transparent
.tabTitle
background-color: #202020
border-color: #424242
float: left
line-height:30px
&:hover
color: black
background-color: white
button.close
color: black
&.active
background-color: #424242
border-color: #424242
&:hover
background-color: #424242
cursor: none
a
color: white
button.close
color: white
a
padding: 5px 15px
cursor: pointer
color: #424242
text-decoration: none
button.close
margin: 4px 0 4px 8px
text-shadow: none
color: white
text-decoration: none
&:hover
color: red
.tab-content
width: 100%
height: calc(100% - 91px)
.tab-pane
width: 100%
height: 100%
@@ -0,0 +1,54 @@
<template name="worklistResult">
<table id="tblStudyList" class="table table-striped noselect">
<thead>
<tr>
<th>
<h5>Patient Name</h5>
<input type="text" class="form-control worklist-search" id="patientName">
</th>
<th>
<h5>Patient ID</h5>
<input type="text" class="form-control worklist-search" id="patientId">
</th>
{{#unless isTouchDevice}}
<th>
<h5>Accession #</h5>
<input type="text" class="form-control worklist-search" id="accessionNumber">
</th>
{{/unless}}
<th>
<h5>Study Date</h5>
<input type="text" class="form-control worklist-search" id="studyDate">
</th>
{{#unless isTouchDevice}}
<th>
<h5>Modality</h5>
<input type="text" class="form-control worklist-search" id="modality">
</th>
{{/unless}}
<th>
<h5>Study Description</h5>
<input type="text" class="form-control worklist-search" id="studyDescription">
</th>
{{#unless isTouchDevice}}
<th>
<h5># Images</h5>
<input type="text" class="form-control worklist-search" id="numImages">
</th>
{{/unless}}
</tr>
</thead>
<tbody id="studyListData">
{{#each studies}}
{{ >worklistStudy }}
{{/each}}
</tbody>
</table>
{{ #unless studies.count }}
{{ >loadingText }}
{{ /unless }}
</template>
@@ -0,0 +1,128 @@
Template.worklistResult.helpers({
/**
* Returns a sorted instance of the WorklistStudies Collection
* by Patient name and Study Date in Ascending order.
*/
studies: function() {
return WorklistStudies.find({}, {
sort: {
patientName: 1,
studyDate: 1
}
});
}
});
// Retrieve all studies
search();
var studyDateFrom;
var studyDateTo;
var checkFrom = false;
var checkTo = false;
var filter;
/**
* Transforms an input string into a search filter for
* the Worklist Search call
*
* @param filter The input string to be searched for
* @returns {*}
*/
function getFilter(filter) {
if (filter && filter.length && filter.substr(filter.length - 1) !== '*') {
filter += '*';
}
return filter;
}
/**
* Search for a value in a string
*/
function isIndexOf(mainVal, searchVal) {
if (mainVal === undefined || mainVal === '' || mainVal.indexOf(searchVal) > -1){
return true;
}
return false;
}
/**
* Replace object if undefined
*/
function replaceUndefinedColumnValue(text) {
if (text === undefined || text === 'undefined') {
return '';
} else {
return text;
}
}
/**
* Convert string to study date
*/
function convertStringToStudyDate(dateStr) {
var y = dateStr.substring(0, 4);
var m = dateStr.substring(4, 6);
var d = dateStr.substring(6, 8);
var newDateStr = y + '/' + m + '/' + d;
return new Date(newDateStr);
}
/**
* Runs a search for studies matching the worklist query parameters
* Inserts the identified studies into the WorklistStudies Collection
*/
function search() {
// Create the filters to be used for the Worklist Search
filter = {
patientName: getFilter($('input#patientName').val()),
patientId: getFilter($('input#patientId').val()),
accessionNumber: getFilter($('input#accessionNumber').val()),
studyDescription: getFilter($('input#studyDescription').val())
};
// Make sure that modality has a reasonable value, since it is occasionally
// returned as 'undefined'
var modality = replaceUndefinedColumnValue($('input#modality').val());
// Clear all current studies
WorklistStudies.remove({});
Meteor.call('WorklistSearch', filter, function(error, studies) {
if (error) {
log.warn(error);
return;
}
if (!studies) {
return;
}
// Loop through all identified studies
studies.forEach(function(study) {
// Search the rest of the parameters that aren't done via the server call
if (isIndexOf(study.modalities, modality) &&
(new Date(studyDateFrom).setHours(0, 0, 0, 0) <= convertStringToStudyDate(study.studyDate) || !checkFrom) &&
(convertStringToStudyDate(study.studyDate) <= new Date(studyDateTo).setHours(0, 0, 0, 0) || !checkTo)) {
// Insert any matching studies into the WorklistStudies Collection
WorklistStudies.insert(study);
}
});
});
}
Template.worklistResult.events({
'keydown input': function(e) {
if (e.which === 13) { // Enter
search();
}
},
'onsearch input': function() {
search();
}
});
@@ -0,0 +1,37 @@
table#tblStudyList
color: #888888
border-collapse: collapse
border-color: gray
thead
background-color: black
white-space: nowrap
&:hover
color: #888888
// TODO: Use padding instead of hidden inputs to keep titles
// at the same height in the header.
#numImages.worklist-search, #modality.worklist-search
visibility: hidden
tbody
tr
padding: 4px
border: 1px solid #282828
background-color: black
&:hover, &:active, &.active
background-color: #009BD2
color: white
td
// This selector is necessary to override bootstrap's 'table' class
color: white
background-color: #009BD2
&:nth-of-type(odd)
background-color: #202020
td
white-space: nowrap
@@ -0,0 +1,31 @@
<template name="worklistStudy">
<tr class="worklistStudy noselect">
<td>
{{formatPN patientName}}
</td>
<td>
{{patientId}}
</td>
{{#unless isTouchDevice}}
<td>
{{accessionNumber}}
</td>
{{/unless}}
<td>
{{formatDA studyDate}}
</td>
{{#unless isTouchDevice}}
<td>
{{modalities}}
</td>
{{/unless}}
<td>
{{studyDescription}}
</td>
{{#unless isTouchDevice}}
<td>
{{numberOfStudyRelatedInstances}}
</td>
{{/unless}}
</tr>
</template>
@@ -0,0 +1,148 @@
// Maybe we should use regular Worklist collection?
WorklistSelectedStudies = new Meteor.Collection(null);
function handleShiftClick(studyRow, data) {
log.info('shiftKey');
var studyInstanceUid = studyRow.attr('studyInstanceUid');
// Select all rows in between these two rows
if (Worklist.previouslySelected) {
var previous = $(Worklist.previouslySelected);
var rowsInBetween;
if (previous.index() < studyRow.index()) {
// The previously selected row is above (lower index) the
// currently selected row.
// Fill in the rows upwards from the previously selected row
rowsInBetween = previous.nextAll('tr');
} else if (previous.index() > studyRow.index()) {
// The previously selected row is below the currently
// selected row.
// Fill in the rows upwards from the previously selected row
rowsInBetween = previous.prevAll('tr');
} else {
// The rows are the same, deselect the current row.
// TODO: CHECK THIS, pretty sure this is the wrong behaviour
WorklistSelectedStudies.remove({});
Worklist.previouslySelected = undefined;
return;
}
// Loop through the rows in between current and previous selected studies
rowsInBetween.each(function() {
var row = $(this);
if (row.hasClass('active')) {
// If we find one that is already selected, do nothing
return;
}
// Get the relevant studyInstanceUid
var studyInstanceUid = row.attr('studyInstanceUid');
// Retrieve the data context through Blaze
var data = Blaze.getData(this);
// Set the current study as selected
WorklistSelectedStudies.insert(data);
row.addClass('active');
// When we reach the currently clicked-on row, stop the loop
return !row.is(studyRow);
});
} else {
// Set the current study as selected
WorklistSelectedStudies.insert(data);
studyRow.addClass('active');
}
}
function handleCtrlClick(studyRow, data) {
log.info('ctrlKey');
var studyInstanceUid = studyRow.attr('studyInstanceUid');
if (studyRow.hasClass('active')) {
studyRow.removeClass('active');
// Find the current studyInstanceUid in the stored list and remove it
WorklistSelectedStudies.remove({
studyInstanceUid: data.studyInstanceUid
});
} else {
// Set the current study as selected
WorklistSelectedStudies.insert(data);
studyRow.addClass('active');
// Set this as the previously selected row, so the user can
// use Shift to select from this point onwards
Worklist.previouslySelected = studyRow;
log.info('Worklist PreviouslySelected set: ' + studyRow.index());
}
}
Template.worklistStudy.events({
'click tr.worklistStudy': function(e) {
var studyRow = $(e.currentTarget);
var data = this;
// Remove the ID so we can directly insert this into our client-side collection
delete data._id;
if (e.shiftKey) {
handleShiftClick(studyRow, data);
} else if (e.ctrlKey || e.metaKey) {
handleCtrlClick(studyRow, data);
} else {
// Select a single study
log.info('Regular click');
// Clear all selected studies
WorklistSelectedStudies.remove({});
$('tr.worklistStudy').removeClass('active');
// Set the previous study to the currently clicked-on study
Worklist.previouslySelected = studyRow;
log.info('Worklist PreviouslySelected set: ' + studyRow.index());
// Set the current study as selected
WorklistSelectedStudies.insert(data);
studyRow.addClass('active');
}
},
'mousedown tr.worklistStudy': function(e) {
// This event handler is meant to handle middle-click on a study
if (e.which !== 2) {
return;
}
var data = this;
var middleClickOnStudy = Worklist.callbacks.middleClickOnStudy;
if (middleClickOnStudy && typeof middleClickOnStudy === 'function') {
middleClickOnStudy(data);
}
},
'dblclick tr.worklistStudy': function(e) {
if (e.which !== 1) {
return;
}
var data = this;
var dblClickOnStudy = Worklist.callbacks.dblClickOnStudy;
if (dblClickOnStudy && typeof dblClickOnStudy === 'function') {
dblClickOnStudy(data);
}
},
'contextmenu tr.worklistStudy': function(e, template) {
$(e.currentTarget).addClass('active');
if (openStudyContextMenu && typeof openStudyContextMenu === 'function') {
e.preventDefault();
openStudyContextMenu(e, template);
return false;
}
}
});
@@ -0,0 +1,2 @@
.worklistStudy
cursor: pointer