First pass at a Hanging Protocol implementation and editor (LT-131)
This commit is contained in:
1 parent
dfcb33153f
commit
2e52d9e3c1
157 files changed
+8869
-23585
No files matched your search
File diff suppressed because it is too large.
Load diff
+16
@@ -0,0 +1,16 @@
|
||||
<template name="confirmDeleteDialog">
|
||||
<div id="confirmDeleteDialog" tabindex="0">
|
||||
{{ #if title }}
|
||||
<h5>{{title}}</h5>
|
||||
{{ else }}
|
||||
<h5>Remove data from Timepoint?</h5>
|
||||
{{/if}}
|
||||
{{ #if text }}
|
||||
<p>{{text}}</p>
|
||||
{{ else }}
|
||||
<p>Are you sure you would like to remove the measurement data at this timepoint?</p>
|
||||
{{/if}}
|
||||
<button id="cancel" class="btn btn-link" tabindex="1">Cancel</button>
|
||||
<button id="confirm" class="btn btn-primary" tabindex="0">OK</button>
|
||||
</div>
|
||||
</template>
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
function closeHandler() {
|
||||
// Hide the lesion dialog
|
||||
$('#confirmDeleteDialog').css('display', 'none');
|
||||
|
||||
// Remove the backdrop
|
||||
$('.removableBackdrop').remove();
|
||||
|
||||
// Remove the callback from the template data
|
||||
delete Template.confirmDeleteDialog.doneCallback;
|
||||
|
||||
// Restore the focus to the active viewport
|
||||
setFocusToActiveViewport();
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the confirmation dialog template and the removable backdrop element
|
||||
*
|
||||
* @param doneCallback A callback
|
||||
* @param options
|
||||
*/
|
||||
showConfirmDialog = function(doneCallback, options) {
|
||||
// Show the backdrop
|
||||
options = options || {};
|
||||
UI.renderWithData(Template.removableBackdrop, options, document.body);
|
||||
|
||||
var confirmDeleteDialog = $('#confirmDeleteDialog');
|
||||
confirmDeleteDialog.remove();
|
||||
|
||||
var viewer = document.getElementById('viewer');
|
||||
UI.renderWithData(Template.confirmDeleteDialog, options, viewer);
|
||||
|
||||
// Make sure the context menu is closed when the user clicks away
|
||||
$('.removableBackdrop').one('mousedown touchstart', function() {
|
||||
closeHandler();
|
||||
});
|
||||
|
||||
confirmDeleteDialog = $('#confirmDeleteDialog');
|
||||
confirmDeleteDialog.css('display', 'block');
|
||||
confirmDeleteDialog.focus();
|
||||
|
||||
if (doneCallback && typeof doneCallback === 'function') {
|
||||
Template.confirmDeleteDialog.doneCallback = doneCallback;
|
||||
}
|
||||
};
|
||||
|
||||
// Global object of key names (TODO: put this somewhere else)
|
||||
keys = {
|
||||
ESC: 27,
|
||||
ENTER: 13
|
||||
};
|
||||
|
||||
Template.confirmDeleteDialog.events({
|
||||
'click #cancel, click #close': function() {
|
||||
closeHandler();
|
||||
},
|
||||
'click #confirm': function() {
|
||||
var doneCallback = Template.confirmDeleteDialog.doneCallback;
|
||||
if (doneCallback && typeof doneCallback === 'function') {
|
||||
doneCallback();
|
||||
}
|
||||
|
||||
closeHandler();
|
||||
},
|
||||
'keydown #confirmDeleteDialog': function(e) {
|
||||
if (e.which === keys.ESC) {
|
||||
closeHandler();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.keyPressAllowed === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
var doneCallback = Template.confirmDeleteDialog.doneCallback;
|
||||
|
||||
// If Enter is pressed, close the dialog
|
||||
if (e.which === keys.ENTER) {
|
||||
if (doneCallback && typeof doneCallback === 'function') {
|
||||
doneCallback();
|
||||
}
|
||||
|
||||
closeHandler();
|
||||
}
|
||||
}
|
||||
});
|
||||
+12
-11
@@ -1,25 +1,26 @@
|
||||
#progressDialog
|
||||
#confirmDeleteDialog
|
||||
display: none
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
right: 0
|
||||
margin: auto
|
||||
width: 20%
|
||||
height: 20%
|
||||
z-index: 100
|
||||
width: 300px
|
||||
min-height: 145px
|
||||
max-height: 300px
|
||||
height: fit-content
|
||||
margin: auto
|
||||
border-radius: 5px
|
||||
padding: 10px 20px 10px 20px
|
||||
background-color: rgba(255,255,255,1)
|
||||
outline: none
|
||||
|
||||
.dialogContent
|
||||
margin-bottom: 20px
|
||||
.btn
|
||||
text-decoration: none
|
||||
|
||||
#message
|
||||
width: 100%
|
||||
#cancel
|
||||
float: left
|
||||
|
||||
span
|
||||
display: inline-block;
|
||||
width: 100%
|
||||
#confirm
|
||||
float: right
|
||||
@@ -1,20 +0,0 @@
|
||||
<template name="hidingPanel">
|
||||
<div class="hidingPanel {{#if isPinned}}pinned{{/if}}">
|
||||
<div class="iconContainer">
|
||||
{{#unless isPinned}}
|
||||
<i class="arrowIcon fa fa-angle-double-right"></i>
|
||||
{{/unless}}
|
||||
<button class="btn btn-sm btn-primary btnPin {{#if isPinned}}active{{/if}}">
|
||||
<i class="fa fa-thumb-tack"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="hidingPanelContent">
|
||||
{{#if studyDateIsShown}}
|
||||
{{> studyDateList}}
|
||||
{{/if}}
|
||||
<div class="studyBrowserContainer">
|
||||
{{> studyBrowser}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,123 +0,0 @@
|
||||
|
||||
function pinPanel() {
|
||||
var hidingPanel = $(".hidingPanel");
|
||||
hidingPanel.css({
|
||||
width: '122px'
|
||||
});
|
||||
|
||||
hidingPanel.find('.hidingPanelContent').css({
|
||||
transform: 'translate(130px)'
|
||||
});
|
||||
|
||||
// Calculate newWidth of viewportAndLesionTable
|
||||
var viewerWidth = $('#viewer').width();
|
||||
var newWidth = 100 - 122 / viewerWidth * 100;
|
||||
$("#viewportAndLesionTable").css('width', newWidth + '%');
|
||||
}
|
||||
|
||||
function unpinPanel() {
|
||||
// Calculate newWidth of viewportAndLesionTable
|
||||
$("#viewportAndLesionTable").css('width', '99%');
|
||||
}
|
||||
|
||||
Template.hidingPanel.events({
|
||||
'mouseover div.hidingPanel': function(e, template) {
|
||||
var isPinned = template.isPinned.get();
|
||||
if (isPinned) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hidingPanel = $(e.currentTarget);
|
||||
hidingPanel.css({
|
||||
width: '122px'
|
||||
});
|
||||
|
||||
// Set panel as open
|
||||
template.panelOpen.set(true);
|
||||
|
||||
// Rotate Arrow Icon
|
||||
$('.arrowIcon').addClass('fa-flip-horizontal');
|
||||
|
||||
// Set panel content opacity
|
||||
//hidingPanel.find('.hidingPanelContent').css('opacity', '1');
|
||||
hidingPanel.find('.hidingPanelContent').css({
|
||||
transform: 'translate(130px)'
|
||||
});
|
||||
},
|
||||
|
||||
'mouseout div.hidingPanel': function(e, template) {
|
||||
var isPinned = template.isPinned.get();
|
||||
if (isPinned) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hidingPanel = $(e.currentTarget);
|
||||
hidingPanel.css({
|
||||
width: '1%'
|
||||
});
|
||||
|
||||
// Set panel as closed
|
||||
template.panelOpen.set(false);
|
||||
|
||||
// Rotate Arrow Icon
|
||||
$('.arrowIcon').removeClass('fa-flip-horizontal');
|
||||
|
||||
// Set panel content opacity
|
||||
//hidingPanel.find('.hidingPanelContent').css('opacity', '0');
|
||||
hidingPanel.find('.hidingPanelContent').css({
|
||||
transform: 'translate(-130px)'
|
||||
});
|
||||
},
|
||||
'click button.btnPin': function(e, template) {
|
||||
var isPinned = template.isPinned.get();
|
||||
isPinned = !isPinned;
|
||||
|
||||
template.isPinned.set(isPinned);
|
||||
|
||||
// Set isPanelPinned of ViewerData
|
||||
var contentId = template.data.contentId;
|
||||
ViewerData[contentId].isPanelPinned = isPinned;
|
||||
Session.set('ViewerData', ViewerData);
|
||||
|
||||
if (isPinned) {
|
||||
pinPanel();
|
||||
} else {
|
||||
unpinPanel();
|
||||
}
|
||||
|
||||
resizeViewportElements();
|
||||
}
|
||||
});
|
||||
|
||||
Template.hidingPanel.helpers({
|
||||
studyDateIsShown: function() {
|
||||
return true;
|
||||
},
|
||||
isPinned: function() {
|
||||
return Template.instance().isPinned.get();
|
||||
}
|
||||
});
|
||||
|
||||
Template.hidingPanel.onRendered(function() {
|
||||
|
||||
var contentId = this.data.contentId;
|
||||
var isPanelPinned = ViewerData[contentId].isPanelPinned;
|
||||
if (isPanelPinned) {
|
||||
pinPanel();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Template.hidingPanel.onCreated(function() {
|
||||
|
||||
this.panelOpen = new ReactiveVar(false);
|
||||
|
||||
var contentId = this.data.contentId;
|
||||
var isPanelPinned = ViewerData[contentId].isPanelPinned;
|
||||
if (!isPanelPinned) {
|
||||
isPanelPinned = false;
|
||||
}
|
||||
this.isPinned = new ReactiveVar(isPanelPinned);
|
||||
|
||||
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
.hidingPanel
|
||||
width: 1%
|
||||
height: 100%
|
||||
position: absolute
|
||||
left: 0
|
||||
top: 70px
|
||||
bottom: 0
|
||||
z-index: 10
|
||||
background-color: black
|
||||
border: 1px solid #666
|
||||
|
||||
&.pinned
|
||||
border: 1px solid transparent
|
||||
|
||||
.iconContainer
|
||||
width: 100%
|
||||
height: 35px
|
||||
overflow: hidden
|
||||
|
||||
.arrowIcon
|
||||
color: white
|
||||
margin: 0px 3px
|
||||
font-size: 15pt
|
||||
line-height: 35px
|
||||
|
||||
.btnPin
|
||||
float: right
|
||||
margin: 3px
|
||||
outline: none
|
||||
|
||||
.hidingPanelContent
|
||||
position: absolute
|
||||
left: -130px
|
||||
width: 100%
|
||||
height: 100%
|
||||
margin: 1px auto
|
||||
|
||||
.studyBrowserContainer
|
||||
height: calc(100% - 150px)
|
||||
|
||||
.hidingPanel, .arrowIcon
|
||||
-o-transition: all 0.3s ease-out
|
||||
-ms-transition: all 0.3s ease-out
|
||||
-moz-transition: all 0.3s ease-out
|
||||
-webkit-transition: all 0.3s ease-out
|
||||
transition: all 0.3s ease-out
|
||||
@@ -1,33 +0,0 @@
|
||||
<template name="layout">
|
||||
<nav class="navbar navbar-default noselect" role="navigation">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/">
|
||||
<img height="30px" src="/images/logo.png">
|
||||
<h4 class="name">Open Health Imaging Foundation</h4>
|
||||
</a>
|
||||
</div>
|
||||
<div id="navbar" class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li>
|
||||
<a href="http://www.ohif.org">About</a>
|
||||
</li>
|
||||
<li>
|
||||
<form class="navbar-form">
|
||||
<div class="form-group">
|
||||
{{> loginButtons align="right"}}
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{{> yield}}
|
||||
</template>
|
||||
@@ -1,47 +0,0 @@
|
||||
.noselect
|
||||
-webkit-touch-callout: none
|
||||
-webkit-user-select: none
|
||||
-khtml-user-select: none
|
||||
-moz-user-select: none
|
||||
-ms-user-select: none
|
||||
user-select: none
|
||||
|
||||
.navbar
|
||||
margin-bottom: 0
|
||||
border-radius: 0
|
||||
color: #888888
|
||||
background-color: #060606
|
||||
border: none
|
||||
font-family: "Sanchez"
|
||||
color: #C1C1C1
|
||||
|
||||
.navbar-default .navbar-nav > li > a
|
||||
color: #C1C1C1
|
||||
font-weight: bold
|
||||
font-size: 12pt
|
||||
|
||||
.navbar-form
|
||||
height: 50px
|
||||
margin: 0
|
||||
padding: 15px 3px
|
||||
font-weight: bold
|
||||
font-size: 12pt
|
||||
line-height: 20px
|
||||
|
||||
.navbar-brand
|
||||
display: inline-block
|
||||
|
||||
img
|
||||
float: left
|
||||
margin-right: 10px
|
||||
margin-top: -5px
|
||||
|
||||
h4.name
|
||||
float: left
|
||||
margin: 0
|
||||
font-family: "Sanchez"
|
||||
color: #C1C1C1
|
||||
|
||||
@media (min-width: 768px)
|
||||
.navbar
|
||||
border-radius: 0px
|
||||
@@ -1,3 +0,0 @@
|
||||
<template name="login">
|
||||
{{> atForm}}
|
||||
</template>
|
||||
@@ -1,3 +0,0 @@
|
||||
<template name="notFound">
|
||||
Not found
|
||||
</template>
|
||||
@@ -1,17 +0,0 @@
|
||||
<template name="progressDialog">
|
||||
<div id="progressDialog" tabindex="0">
|
||||
<div class="dialogHeader">
|
||||
<h5>{{progressDialogTitle}}</h5>
|
||||
</div>
|
||||
<div class="dialogContent">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-striped active" role="progressbar" style="width:{{progressStatus}}">
|
||||
{{progressStatus}}
|
||||
</div>
|
||||
</div>
|
||||
<div id="message">
|
||||
{{{progressMessage}}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,100 +0,0 @@
|
||||
progressDialog = {
|
||||
/**
|
||||
* Shows progress dialog
|
||||
* @param {Object} settings - Settings used by progress dialog
|
||||
* @param {string} settings.title
|
||||
* @param {int} settings.numberOfCompleted - The value of progress dialog
|
||||
* @param {int} settings.numberOfTotal - The max value of progress dialog
|
||||
* @param {Object} [settings.messageParams] - Show a message in progress dialog by setting message and messageType
|
||||
* @param {string} [settings.messageParams.message="Progress"] - The message will be shown in the progress dialog
|
||||
* @param {string} [settings.messageParams.messageType="info"] - Sets ui, accepts bootstrap predefined classes such as success, info, warning, danger
|
||||
*/
|
||||
'show': function(settings) {
|
||||
// Set dialog settings
|
||||
Session.set("progressDialogSettings", settings);
|
||||
$('#progressDialog').css('display', 'block');
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the value of the progress dialog
|
||||
* @param {int} numberOfCompleted
|
||||
*/
|
||||
'update': function(numberOfCompleted) {
|
||||
var progressDialogSettings = Session.get("progressDialogSettings");
|
||||
progressDialogSettings.numberOfCompleted = numberOfCompleted;
|
||||
|
||||
Session.set("progressDialogSettings", progressDialogSettings);
|
||||
|
||||
if (progressDialogSettings.numberOfCompleted === progressDialogSettings.numberOfTotal) {
|
||||
progressDialog.close();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Closes the progress dialog
|
||||
*/
|
||||
'close': function() {
|
||||
// Reset progressDialogSettings session
|
||||
resetDialogSettingsSession();
|
||||
|
||||
// Close dialog
|
||||
$('#progressDialog').css('display', 'none');
|
||||
},
|
||||
/**
|
||||
* Shows a message in the progress dialog
|
||||
* @param {Object} messageParams
|
||||
* @param {string} messageParams.message
|
||||
* @param {string} messageParams.messageType
|
||||
*/
|
||||
'setMessage': function(messageParams) {
|
||||
var progressDialogSettings = Session.get("progressDialogSettings");
|
||||
if (!messageParams.messageType || messageParams.messageType == '') {
|
||||
messageParams.messageType = 'info';
|
||||
}
|
||||
progressDialogSettings.messageParams = messageParams;
|
||||
Session.set("progressDialogSettings", progressDialogSettings);
|
||||
}
|
||||
};
|
||||
|
||||
Template.progressDialog.helpers({
|
||||
'progressDialogTitle': function () {
|
||||
if (Session.get("progressDialogSettings") && Session.get("progressDialogSettings").title) {
|
||||
return Session.get("progressDialogSettings").title;
|
||||
}
|
||||
|
||||
return "Progress:";
|
||||
},
|
||||
'progressStatus': function() {
|
||||
var numberOfCompleted = 0;
|
||||
if (Session.get("progressDialogSettings") && Session.get("progressDialogSettings").numberOfCompleted) {
|
||||
numberOfCompleted = Session.get("progressDialogSettings").numberOfCompleted;
|
||||
}
|
||||
|
||||
var numberofTotal = 1;
|
||||
if (Session.get("progressDialogSettings") && Session.get("progressDialogSettings").numberOfTotal) {
|
||||
numberofTotal = Session.get("progressDialogSettings").numberOfTotal;
|
||||
}
|
||||
|
||||
return parseInt((numberOfCompleted / numberofTotal) * 100) + "%";
|
||||
},
|
||||
'progressMessage': function() {
|
||||
var progressDialogSettings = Session.get("progressDialogSettings");
|
||||
var messageParams = progressDialogSettings && progressDialogSettings.messageParams || false;
|
||||
if (messageParams && messageParams.message) {
|
||||
return '<span class="label label-'+messageParams.messageType+'">'+messageParams.message+'</span>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Resets progressDialogSettings
|
||||
function resetDialogSettingsSession() {
|
||||
Session.set("progressDialogSettings",
|
||||
{
|
||||
title: 'Progress',
|
||||
numberOfCompleted: 0,
|
||||
numberOfTotal: 0,
|
||||
messageParams: {message: null, messageType: 'info'}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<template name="studyNotFound">
|
||||
<div class="studyNotFound">
|
||||
<h5>Study not found</h5>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,6 +0,0 @@
|
||||
.studyNotFound
|
||||
margin: 30px
|
||||
|
||||
h5
|
||||
text-align: center
|
||||
color: #888888
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<template name="relatedStudySelect">
|
||||
<div class="relatedStudySelect">
|
||||
<label>Study:</label>
|
||||
<span class="loading"><i class="fa fa-spinner fa-spin"></i></span>
|
||||
<select id="selectStudyDate">
|
||||
{{#each relatedStudies}}
|
||||
<option value="{{studyInstanceUid}}" selected={{selected}}>{{formatDA studyDate}}: {{studyDescription}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
Template.relatedStudySelect.helpers({
|
||||
/**
|
||||
* Returns an array of studies that are related to the current study by patient ID.
|
||||
* The value for 'selected' for the currently loaded study is set to true, so that
|
||||
* this becomes the current option in the combo box.
|
||||
*
|
||||
* @returns {*} Array of studies that are related to the current study by patient ID
|
||||
*/
|
||||
relatedStudies: function() {
|
||||
// Check which study is currently loaded into the study browser
|
||||
var currentStudyInBrowser = ViewerStudies.findOne({
|
||||
selected: true
|
||||
});
|
||||
|
||||
// Find all studies in the Worklist which have the same patientId as the currently selected study
|
||||
var relatedStudies = WorklistStudies.find({
|
||||
patientId: currentStudyInBrowser.patientId
|
||||
}, {
|
||||
sort: {
|
||||
studyDate: -1
|
||||
}
|
||||
}).fetch();
|
||||
|
||||
// If no Study / Timepoint associated studies exist, just
|
||||
// return the list of loaded studies
|
||||
if (!relatedStudies.length) {
|
||||
return ViewerStudies.find();
|
||||
}
|
||||
|
||||
// Modify the array of related studies so the default option is the currently selected study
|
||||
relatedStudies.forEach(function(study, index) {
|
||||
// If the studyInstanceUid matches that of the current study in the browser,
|
||||
// Set this to 'selected', so that it becomes the default option
|
||||
if (study.studyInstanceUid === currentStudyInBrowser.studyInstanceUid) {
|
||||
relatedStudies[index].selected = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Use this array to populate the combo box
|
||||
return relatedStudies;
|
||||
}
|
||||
});
|
||||
|
||||
Template.relatedStudySelect.events({
|
||||
/**
|
||||
* When the study date selector combo box is changed, we will
|
||||
* hide the select box, temporarily display a loading sign, and grab
|
||||
* the selected study. Once the study has been retrieved it is added
|
||||
* into the ViewerStudies collection and set as selected. This reactively
|
||||
* populated the thumbnail browser.
|
||||
*
|
||||
* @param e The select box change event
|
||||
*/
|
||||
'change select#selectStudyDate': function(e) {
|
||||
var selectBox = $(e.currentTarget);
|
||||
var studyInstanceUid = selectBox.val();
|
||||
|
||||
// Hide the select box
|
||||
selectBox.css('display', 'none');
|
||||
|
||||
// Show the loading indicator
|
||||
var loadingIndicator = selectBox.siblings('.loading');
|
||||
loadingIndicator.css('display', 'block');
|
||||
|
||||
getStudyMetadata(studyInstanceUid, function(study) {
|
||||
sortStudy(study);
|
||||
|
||||
// Hide the loading indicator
|
||||
loadingIndicator.css('display', 'none');
|
||||
|
||||
// Show the select box again
|
||||
selectBox.css('display', 'block');
|
||||
|
||||
// Set "Selected" to false for the entire collection
|
||||
ViewerStudies.update({}, {
|
||||
$set: {
|
||||
selected: false
|
||||
}
|
||||
}, {
|
||||
multi: true
|
||||
});
|
||||
|
||||
// Check if this study already exists in the ViewerStudies collection
|
||||
// of loaded studies. If it does, set it's 'selected' value to true.
|
||||
var existingStudy = ViewerStudies.findOne({
|
||||
studyInstanceUid: studyInstanceUid
|
||||
});
|
||||
|
||||
if (existingStudy) {
|
||||
// Set the current finding in the collection to true
|
||||
ViewerStudies.update(existingStudy._id, {
|
||||
$set: {
|
||||
selected: true
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If the study does not exist, add the 'selected' key to the object
|
||||
// with the value True, and insert it into the ViewerStudies Collection
|
||||
study.selected = true;
|
||||
ViewerStudies.insert(study);
|
||||
});
|
||||
}
|
||||
});
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
.relatedStudySelect
|
||||
margin: 0 auto 15px
|
||||
text-align: center
|
||||
width: 100px
|
||||
|
||||
#selectStudyDate
|
||||
color: black
|
||||
background-color: #666
|
||||
border-color: #666
|
||||
width: 96%
|
||||
margin: 0 auto
|
||||
|
||||
label
|
||||
font-weight: normal
|
||||
color: #666
|
||||
float: left
|
||||
padding-left: 2%
|
||||
|
||||
.loading
|
||||
display: none
|
||||
color: white
|
||||
text-align: center
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<div class="studyBrowser">
|
||||
<div class="scrollableStudyThumbnails">
|
||||
{{ #each studies }}
|
||||
{{ >thumbnails }}
|
||||
{{#each thumbnails}}
|
||||
{{ >thumbnailEntry }}
|
||||
{{/each}}
|
||||
{{ /each }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
Template.studyBrowser.helpers({
|
||||
studies : function() {
|
||||
return ViewerStudies.find({selected: true});
|
||||
},
|
||||
thumbnails: function() {
|
||||
var study = this;
|
||||
var stacks = createStacks(study);
|
||||
|
||||
var array = [];
|
||||
stacks.forEach(function(stack, index) {
|
||||
array.push({
|
||||
thumbnailIndex: index,
|
||||
stack: stack
|
||||
});
|
||||
});
|
||||
return array;
|
||||
}
|
||||
});
|
||||
@@ -184,9 +184,10 @@ function thumbnailDragEndHandler(e, target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var viewportIndex = $('.imageViewerViewport').index(element);
|
||||
|
||||
// Rerender the viewport using the drag and drop data
|
||||
rerenderViewportWithNewSeries(element, OHIF.viewer.dragAndDropData);
|
||||
Session.get('UseHangingProtocol', false);
|
||||
layoutManager.rerenderViewportWithNewSeries(viewportIndex, OHIF.viewer.dragAndDropData);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<template name="thumbnails">
|
||||
{{#each thumbnails}}
|
||||
{{ >thumbnailEntry }}
|
||||
{{/each}}
|
||||
</template>
|
||||
@@ -1,15 +0,0 @@
|
||||
Template.thumbnails.helpers({
|
||||
thumbnails: function() {
|
||||
var study = this;
|
||||
var stacks = createStacks(study);
|
||||
|
||||
var array = [];
|
||||
stacks.forEach(function(stack, index) {
|
||||
array.push({
|
||||
thumbnailIndex: index,
|
||||
stack: stack
|
||||
});
|
||||
});
|
||||
return array;
|
||||
}
|
||||
});
|
||||
@@ -19,7 +19,6 @@ function updateFramerate(rate) {
|
||||
|
||||
// If the movie is playing, stop/start to update the framerate
|
||||
if (isPlaying()) {
|
||||
var element = getActiveViewportElement();
|
||||
cornerstoneTools.stopClip(element);
|
||||
cornerstoneTools.playClip(element);
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
<template name="imageViewerViewports">
|
||||
<template name="gridLayout">
|
||||
<div id='imageViewerViewports'>
|
||||
{{ #each viewerWindow }}
|
||||
{{ #each viewports }}
|
||||
<div class="viewportContainer" style="height:{{height}}%;width:{{width}}%;">
|
||||
<div class="removable">
|
||||
{{ >imageViewerViewport}}
|
||||
@@ -0,0 +1,38 @@
|
||||
Template.gridLayout.helpers({
|
||||
height: function() {
|
||||
var rows = this.rows || 1;
|
||||
return 100 / rows;
|
||||
},
|
||||
width: function() {
|
||||
var columns = this.columns || 1;
|
||||
return 100 / columns;
|
||||
},
|
||||
viewports: function() {
|
||||
var numViewports = this.rows * this.columns;
|
||||
var viewportData = this.viewportData;
|
||||
var numViewportsWithData = this.viewportData.length;
|
||||
|
||||
if (numViewportsWithData < numViewports) {
|
||||
var difference = numViewports - numViewportsWithData;
|
||||
for (var i = 0; i < difference; i++) {
|
||||
viewportData.push({
|
||||
viewportIndex: numViewportsWithData + i + 1,
|
||||
rows: this.rows,
|
||||
columns: this.columns
|
||||
});
|
||||
}
|
||||
} else if (numViewportsWithData > numViewports) {
|
||||
return viewportData.slice(0, numViewports);
|
||||
}
|
||||
|
||||
return viewportData;
|
||||
}
|
||||
});
|
||||
|
||||
Template.gridLayout.onRendered(function() {
|
||||
OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer('CornerstoneNewImage', cornerstoneTools.updateImageSynchronizer);
|
||||
});
|
||||
|
||||
Template.gridLayout.onDestroyed(function() {
|
||||
OHIF.viewer.updateImageSynchronizer.destroy();
|
||||
});
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
#imageViewerViewports
|
||||
height: calc(100% - 30px)
|
||||
height: 100%
|
||||
width: 100%
|
||||
|
||||
|
||||
.viewportContainer
|
||||
float: left
|
||||
border: 2px solid #777
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
<template name="hangingProtocolButtons">
|
||||
<button id="previousPresentationGroup" type="button"
|
||||
class="imageViewerCommand btn btn-sm btn-default"
|
||||
data-container="body"
|
||||
data-toggle="tooltip"
|
||||
data-placement="bottom"
|
||||
title="Previous presentation group"
|
||||
disabled="{{ #unless isPreviousAvailable }} true {{ /unless }}">
|
||||
<span class="fa fa-step-backward"></span>
|
||||
</button>
|
||||
<button id="nextPresentationGroup" type="button"
|
||||
class="imageViewerCommand btn btn-sm btn-default"
|
||||
data-container="body"
|
||||
data-toggle="tooltip"
|
||||
data-placement="bottom"
|
||||
title="Next presentation group"
|
||||
disabled="{{ #unless isNextAvailable }} true {{ /unless }}">
|
||||
<span class="fa fa-step-forward"></span>
|
||||
</button>
|
||||
</template>
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
Template.hangingProtocolButtons.helpers({
|
||||
isNextAvailable: function() {
|
||||
var presentationGroup = Session.get('WindowManagerPresentationGroup');
|
||||
var numPresentationGroups = WindowManager.getNumPresentationGroups();
|
||||
return presentationGroup < numPresentationGroups;
|
||||
},
|
||||
isPreviousAvailable: function() {
|
||||
var presentationGroup = Session.get('WindowManagerPresentationGroup');
|
||||
return presentationGroup > 1;
|
||||
}
|
||||
});
|
||||
+57
-35
@@ -21,6 +21,7 @@ function loadSeriesIntoViewport(data, templateData) {
|
||||
// The viewport index is often used to store information about a viewport element
|
||||
var element = data.element;
|
||||
var viewportIndex = $('.imageViewerViewport').index(element);
|
||||
layoutManager.viewportData[viewportIndex].viewportIndex = viewportIndex;
|
||||
|
||||
// Get the contentID of the current worklist tab, if the viewport is running
|
||||
// alongside the worklist package
|
||||
@@ -140,15 +141,47 @@ function loadSeriesIntoViewport(data, templateData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If there is a saved object containing Cornerstone viewport data
|
||||
// (e.g. scale, invert, window settings) in the input data, apply it now.
|
||||
//
|
||||
// Otherwise, display the loaded image in the viewport element with the
|
||||
// default viewport settings.
|
||||
if (data.viewport) {
|
||||
// Update the enabled element with the image and viewport data
|
||||
// This is not usually necessary, but we need them stored in case
|
||||
// a sopClassUid-specific viewport setting is present.
|
||||
enabledElement.image = image;
|
||||
enabledElement.viewport = cornerstone.getDefaultViewport(enabledElement.canvas, image);
|
||||
|
||||
// Check if there are default viewport settings for this sopClassUid
|
||||
if (!series.instances || !series.instances.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var instance = series.instances[0];
|
||||
var instanceClassViewport = getInstanceClassDefaultViewport(instance, enabledElement, image.imageId);
|
||||
|
||||
// If there are sopClassUid-specific viewport settings, apply them
|
||||
if (instanceClassViewport) {
|
||||
cornerstone.displayImage(element, image, instanceClassViewport);
|
||||
|
||||
// Mark that this element should not be fit to the window in the resize listeners
|
||||
// TODO: Find another way to do this?
|
||||
enabledElement.fitToWindow = false;
|
||||
|
||||
// Resize the canvas to fit the current viewport element size.
|
||||
cornerstone.resize(element, false);
|
||||
} else if (data.viewport) {
|
||||
// If there is a saved object containing Cornerstone viewport data
|
||||
// (e.g. scale, invert, window settings) in the input data, apply it now.
|
||||
cornerstone.displayImage(element, image, data.viewport);
|
||||
|
||||
// Resize the canvas to fit the current viewport element size. Fit the displayed
|
||||
// image to the canvas dimensions.
|
||||
cornerstone.resize(element, true);
|
||||
} else {
|
||||
// If no saved viewport settings or modality-specific settings exists,
|
||||
// display the loaded image in the viewport element with no loaded viewport
|
||||
// settings.
|
||||
cornerstone.displayImage(element, image);
|
||||
|
||||
// Resize the canvas to fit the current viewport element size. Fit the displayed
|
||||
// image to the canvas dimensions.
|
||||
cornerstone.resize(element, true);
|
||||
}
|
||||
|
||||
// Remove the data for this viewport from the ViewportLoading object
|
||||
@@ -159,10 +192,6 @@ function loadSeriesIntoViewport(data, templateData) {
|
||||
// (e.g. hide the progress text box)
|
||||
endLoadingHandler(element);
|
||||
|
||||
// Resize the canvas to fit the current viewport element size. Fit the displayed
|
||||
// image to the canvas dimensions.
|
||||
cornerstone.resize(element, true);
|
||||
|
||||
// Remove the 'empty' class from the viewport to hide any instruction text
|
||||
element.classList.remove('empty');
|
||||
|
||||
@@ -172,7 +201,7 @@ function loadSeriesIntoViewport(data, templateData) {
|
||||
$(element).siblings('.imageViewerViewportOverlay').show();
|
||||
|
||||
// Add stack state managers for the stack tool, CINE tool, and reference lines
|
||||
cornerstoneTools.addStackStateManager(element, [ 'stack', 'playClip', 'referenceLines' ]);
|
||||
cornerstoneTools.addStackStateManager(element, ['stack', 'playClip', 'referenceLines']);
|
||||
|
||||
// Get the current viewport settings
|
||||
var viewport = cornerstone.getViewport(element);
|
||||
@@ -206,7 +235,7 @@ function loadSeriesIntoViewport(data, templateData) {
|
||||
// Use the tool manager to enable the currently active tool for this
|
||||
// newly rendered element
|
||||
var activeTool = toolManager.getActiveTool();
|
||||
toolManager.setActiveTool(activeTool, [ element ]);
|
||||
toolManager.setActiveTool(activeTool, [element]);
|
||||
|
||||
// Define a function to run whenever the Cornerstone viewport is rendered
|
||||
// (e.g. following a change of window or zoom)
|
||||
@@ -220,6 +249,7 @@ function loadSeriesIntoViewport(data, templateData) {
|
||||
// Save the current viewport into the ViewerData global variable, as well as the
|
||||
// Meteor Session. This lets the viewport be saved/reloaded on a hot-code reload
|
||||
var viewport = cornerstone.getViewport(element);
|
||||
layoutManager.viewportData[viewportIndex].viewport = viewport;
|
||||
ViewerData[contentId].loadedSeriesData[viewportIndex].viewport = viewport;
|
||||
Session.set('ViewerData', ViewerData);
|
||||
}
|
||||
@@ -240,6 +270,7 @@ function loadSeriesIntoViewport(data, templateData) {
|
||||
// This allows the template helpers to update reactively
|
||||
templateData.imageId = eventData.enabledElement.image.imageId;
|
||||
Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
|
||||
layoutManager.viewportData[viewportIndex].imageId = eventData.enabledElement.image.imageId;
|
||||
|
||||
// Get the element and stack data
|
||||
var element = e.target;
|
||||
@@ -260,6 +291,7 @@ function loadSeriesIntoViewport(data, templateData) {
|
||||
var stack = cornerstoneTools.getToolState(element, 'stack');
|
||||
if (stack && stack.data.length && stack.data[0].imageIds.length > 1) {
|
||||
var imageIdIndex = stack.data[0].imageIds.indexOf(templateData.imageId);
|
||||
layoutManager.viewportData[viewportIndex].currentImageIdIndex = imageIdIndex;
|
||||
ViewerData[contentId].loadedSeriesData[viewportIndex].currentImageIdIndex = imageIdIndex;
|
||||
Session.set('ViewerData', ViewerData);
|
||||
}
|
||||
@@ -327,14 +359,17 @@ function loadSeriesIntoViewport(data, templateData) {
|
||||
$(element).off(allCornerstoneEvents, sendActivationTrigger);
|
||||
$(element).on(allCornerstoneEvents, sendActivationTrigger);
|
||||
|
||||
// Store the current series data inside a global variable
|
||||
OHIF.viewer.loadedSeriesData[viewportIndex] = {
|
||||
// Store the current series data inside the Layout Manager
|
||||
layoutManager.viewportData[viewportIndex] = {
|
||||
imageId: imageId,
|
||||
studyInstanceUid: data.studyInstanceUid,
|
||||
seriesInstanceUid: data.seriesInstanceUid,
|
||||
currentImageIdIndex: data.currentImageIdIndex,
|
||||
viewport: viewport
|
||||
viewport: viewport,
|
||||
viewportIndex: viewportIndex
|
||||
};
|
||||
ViewerData[contentId].loadedSeriesData = OHIF.viewer.loadedSeriesData;
|
||||
|
||||
ViewerData[contentId].loadedSeriesData = layoutManager.viewportData;
|
||||
|
||||
// Check if image plane (orientation / loction) data is present for the current image
|
||||
var imagePlane = cornerstoneTools.metaData.get('imagePlane', image.imageId);
|
||||
@@ -443,7 +478,7 @@ Meteor.startup(function() {
|
||||
});
|
||||
|
||||
Template.imageViewerViewport.onCreated(function() {
|
||||
log.info('imageViewerViewport onCreated');
|
||||
log.info('imageViewerViewport onCreated');
|
||||
});
|
||||
|
||||
Template.imageViewerViewport.onRendered(function() {
|
||||
@@ -487,23 +522,6 @@ Template.imageViewerViewport.onRendered(function() {
|
||||
});
|
||||
var seriesInstanceUid = this.data.seriesInstanceUid;
|
||||
|
||||
// TODO: This code block might be refactored
|
||||
// Load previous measurement study when reloading a patient
|
||||
if (!study) {
|
||||
getStudyMetadata(this.data.studyInstanceUid, function(study) {
|
||||
// Once we have retrieved the data, we sort the series' by series
|
||||
// and instance number in ascending order
|
||||
if (!study) {
|
||||
return;
|
||||
}
|
||||
|
||||
sortStudy(study);
|
||||
data.study = study;
|
||||
|
||||
setSeries(data, seriesInstanceUid, templateData);
|
||||
})
|
||||
}
|
||||
|
||||
data.study = study;
|
||||
setSeries(data, seriesInstanceUid, templateData);
|
||||
});
|
||||
@@ -539,5 +557,9 @@ Template.imageViewerViewport.events({
|
||||
'click .imageViewerViewport': function(e) {
|
||||
var viewportIndex = $('.imageViewerViewport').index(e.currentTarget);
|
||||
Session.set('activeViewport', viewportIndex);
|
||||
},
|
||||
'dblclick .imageViewerViewport': function(e) {
|
||||
var viewportIndex = $('.imageViewerViewport').index(e.currentTarget);
|
||||
layoutManager.toggleEnlargement(viewportIndex);
|
||||
}
|
||||
});
|
||||
});
|
||||
-201
@@ -1,201 +0,0 @@
|
||||
ViewerWindows = new Meteor.Collection(null);
|
||||
ViewerWindows._debugName = 'ViewerWindows';
|
||||
|
||||
Template.imageViewerViewports.helpers({
|
||||
height: function() {
|
||||
var viewportRows = this.viewportRows || 1;
|
||||
return 100 / viewportRows;
|
||||
},
|
||||
width: function() {
|
||||
var viewportColumns = this.viewportColumns || 1;
|
||||
return 100 / viewportColumns;
|
||||
},
|
||||
viewerWindow: function() {
|
||||
log.info('ViewerWindows');
|
||||
//log.info(ViewerWindows.find().fetch());
|
||||
ViewerWindows.remove({});
|
||||
|
||||
log.info('imageViewerViewports viewportArray');
|
||||
|
||||
var viewportRows = this.viewportRows || 1;
|
||||
var viewportColumns = this.viewportColumns || 1;
|
||||
|
||||
var contentId = Session.get('activeContentId');
|
||||
if (this.viewportRows && this.viewportColumns) {
|
||||
viewportRows = this.viewportRows || 1;
|
||||
viewportColumns = this.viewportColumns || 1;
|
||||
} else if (ViewerData[contentId].viewportRows && ViewerData[contentId].viewportColumns) {
|
||||
viewportRows = ViewerData[contentId].viewportRows;
|
||||
viewportColumns = ViewerData[contentId].viewportColumns;
|
||||
}
|
||||
|
||||
var viewportData;
|
||||
if (!$.isEmptyObject(ViewerData[contentId].loadedSeriesData)) {
|
||||
viewportData = ViewerData[contentId].loadedSeriesData;
|
||||
}
|
||||
|
||||
var inputData = {
|
||||
viewportColumns: viewportColumns,
|
||||
viewportRows: viewportRows
|
||||
};
|
||||
|
||||
inputData.DisplaySetPresentationGroup = Session.get('WindowManagerPresentationGroup');
|
||||
var hangingProtocolViewportData = WindowManager.getHangingProtocol(inputData);
|
||||
if (Session.get('UseHangingProtocol')) {
|
||||
viewportData = hangingProtocolViewportData.viewports;
|
||||
viewportRows = hangingProtocolViewportData.viewportRows || viewportRows;
|
||||
viewportColumns = hangingProtocolViewportData.viewportColumns || viewportColumns;
|
||||
}
|
||||
|
||||
// Update viewerData
|
||||
ViewerData[contentId].viewportRows = viewportRows;
|
||||
ViewerData[contentId].viewportColumns = viewportColumns;
|
||||
Session.set('ViewerData', ViewerData);
|
||||
|
||||
this.viewportRows = viewportRows;
|
||||
this.viewportColumns = viewportColumns;
|
||||
|
||||
var numViewports = viewportRows * viewportColumns;
|
||||
for (var i = 0; i < numViewports; ++i) {
|
||||
var data = {
|
||||
viewportIndex: i,
|
||||
// These two are necessary because otherwise the width and height helpers
|
||||
// don't get the right data context. Seems to be related to the "each" loop.
|
||||
viewportColumns: viewportColumns,
|
||||
viewportRows: viewportRows
|
||||
};
|
||||
|
||||
if (viewportData && !$.isEmptyObject(viewportData[i])) {
|
||||
data.seriesInstanceUid = viewportData[i].seriesInstanceUid;
|
||||
data.studyInstanceUid = viewportData[i].studyInstanceUid;
|
||||
data.currentImageIdIndex = viewportData[i].currentImageIdIndex;
|
||||
data.viewport = viewportData[i].viewport;
|
||||
} else if (hangingProtocolViewportData && !$.isEmptyObject(hangingProtocolViewportData.viewports[i])) {
|
||||
data.seriesInstanceUid = hangingProtocolViewportData.viewports[i].seriesInstanceUid;
|
||||
data.studyInstanceUid = hangingProtocolViewportData.viewports[i].studyInstanceUid;
|
||||
data.currentImageIdIndex = hangingProtocolViewportData.viewports[i].currentImageIdIndex;
|
||||
data.viewport = hangingProtocolViewportData.viewports[i].viewport;
|
||||
}
|
||||
|
||||
ViewerWindows.insert(data);
|
||||
}
|
||||
|
||||
// Here we will find out if we need to load any other studies into the viewer
|
||||
|
||||
// We will make a list of unique studyInstanceUids
|
||||
var uniqueStudyInstanceUids = [];
|
||||
|
||||
// Meteor doesn't support Mongo's 'distinct' function, so we have to do this in a loop
|
||||
var windows = ViewerWindows.find({}, {
|
||||
reactive: false
|
||||
}).fetch();
|
||||
|
||||
windows.forEach(function(window) {
|
||||
var studyInstanceUid = window.studyInstanceUid;
|
||||
if (!studyInstanceUid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If this studyInstanceUid is already in the list, stop here
|
||||
if (uniqueStudyInstanceUids.indexOf(studyInstanceUid) > -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, add it to the list
|
||||
uniqueStudyInstanceUids.push(studyInstanceUid);
|
||||
|
||||
// If any of the associated studies is not already loaded, load it now
|
||||
var loadedStudy = ViewerStudies.findOne({
|
||||
studyInstanceUid: studyInstanceUid
|
||||
}, {
|
||||
reactive: false
|
||||
});
|
||||
|
||||
if (!loadedStudy) {
|
||||
// Load the study
|
||||
getStudyMetadata(studyInstanceUid, function(study) {
|
||||
log.info("imageViewerViewports GetStudyMetadata: " + studyInstanceUid);
|
||||
|
||||
// Sort the study's series and instances by series and instance number
|
||||
sortStudy(study);
|
||||
|
||||
// Insert it into the ViewerStudies Collection
|
||||
ViewerStudies.insert(study);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return ViewerWindows.find({}, {
|
||||
reactive: false
|
||||
}).fetch();
|
||||
}
|
||||
});
|
||||
|
||||
var savedSeriesData,
|
||||
savedViewportRows,
|
||||
savedViewportColumns;
|
||||
|
||||
Template.imageViewerViewports.events({
|
||||
'CornerstoneMouseDoubleClick .imageViewerViewport': function(e) {
|
||||
var container = $('.viewerMain').get(0);
|
||||
var data;
|
||||
var contentId = this.contentId || $('#viewer').parents('.tab-pane.active').attr('id');
|
||||
|
||||
// If there is more than one viewport on screen
|
||||
// And one of them is double-clicked, it should be rendered alone
|
||||
// If it is double-clicked again, the viewer should revert to the previous layout
|
||||
if ($(e.currentTarget).hasClass('zoomed')) {
|
||||
// Revert to saved settings
|
||||
ViewerData[contentId].loadedSeriesData = $.extend(true, {}, savedSeriesData);
|
||||
ViewerData[contentId].viewportRows = savedViewportRows;
|
||||
ViewerData[contentId].viewportColumns = savedViewportColumns;
|
||||
|
||||
savedViewportRows = 0;
|
||||
savedViewportColumns = 0;
|
||||
|
||||
data = {
|
||||
viewportRows: ViewerData[contentId].viewportRows,
|
||||
viewportColumns: ViewerData[contentId].viewportColumns
|
||||
};
|
||||
|
||||
// Render the imageViewerViewports template with these settings
|
||||
$('#imageViewerViewports').remove();
|
||||
UI.renderWithData(Template.imageViewerViewports, data, container);
|
||||
|
||||
// Remove the 'zoomed' class from any viewports
|
||||
$('.imageViewerViewport').removeClass('zoomed');
|
||||
} else {
|
||||
// Zoom to single viewport
|
||||
|
||||
// If only one viewport is on-screen, stop here
|
||||
if (ViewerData[contentId].viewportRows === 1 &&
|
||||
ViewerData[contentId].viewportColumns === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the current settings
|
||||
savedSeriesData = $.extend(true, {}, ViewerData[contentId].loadedSeriesData);
|
||||
savedViewportRows = ViewerData[contentId].viewportRows;
|
||||
savedViewportColumns = ViewerData[contentId].viewportColumns;
|
||||
|
||||
// Get the clicked-on viewport's index
|
||||
var viewportIndex = this.viewportIndex;
|
||||
|
||||
// Set the first viewport's data to be the same as the currently clicked-on viewport
|
||||
ViewerData[contentId].loadedSeriesData[0] = ViewerData[contentId].loadedSeriesData[viewportIndex];
|
||||
|
||||
// Set the basic template data
|
||||
data = {
|
||||
viewportRows: 1,
|
||||
viewportColumns: 1
|
||||
};
|
||||
|
||||
// Render the imageViewerViewports template with these settings
|
||||
$('#imageViewerViewports').remove();
|
||||
UI.renderWithData(Template.imageViewerViewports, data, container);
|
||||
|
||||
// Add the 'zoomed' class to the lone remaining viewport
|
||||
$('.imageViewerViewport').eq(0).addClass('zoomed');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<template name="layoutButton">
|
||||
<button id="layout" type="button" class="btn btn-sm btn-default dropdown-toggle" data-container="body" data-toggle="dropdown" aria-expanded="false" data-placement="right" title="Layout" rel="tooltip">
|
||||
<span class="fa fa-th-large"></span>
|
||||
</button>
|
||||
{{ >layoutChooser }}
|
||||
</template>
|
||||
@@ -1,20 +1,29 @@
|
||||
<template name="layoutChooser">
|
||||
<div class="layoutChooser pull-right dropdown-menu" role="menu">
|
||||
<div class="layoutChooser pull-left dropdown-menu" role="menu">
|
||||
<table>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -33,19 +33,18 @@ Template.layoutChooser.events({
|
||||
highlightCells(evt.currentTarget);
|
||||
},
|
||||
'click .layoutChooser table td': function(evt) {
|
||||
$('#imageViewerViewports').remove();
|
||||
|
||||
var currentCell = $(evt.currentTarget);
|
||||
var rowIndex = currentCell.closest('tr').index();
|
||||
var columnIndex = currentCell.index();
|
||||
|
||||
// Add 1 because the indices start from zero
|
||||
var data = {
|
||||
viewportRows: rowIndex + 1,
|
||||
viewportColumns: columnIndex + 1,
|
||||
var layoutProps = {
|
||||
rows: rowIndex + 1,
|
||||
columns: columnIndex + 1
|
||||
};
|
||||
|
||||
var container = $(".viewerMain").get(0);
|
||||
UI.renderWithData(Template.imageViewerViewports, data, container);
|
||||
|
||||
layoutManager.layoutTemplateName = 'gridLayout';
|
||||
layoutManager.layoutProps = layoutProps;
|
||||
layoutManager.updateViewports();
|
||||
}
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
.layoutChooser
|
||||
width: 75px
|
||||
min-width: 75px // to override bootstrap's dropdown-menu class
|
||||
height: 75px
|
||||
width: 92px
|
||||
height: 92px
|
||||
min-width: 92px // to override bootstrap's dropdown-menu class
|
||||
min-height: 92px
|
||||
|
||||
table
|
||||
margin: 0 auto
|
||||
@@ -11,7 +12,7 @@
|
||||
border: 2px solid black
|
||||
width: 20px
|
||||
height: 20px
|
||||
transition: background-color 0.1s linear
|
||||
transition: background-color 0.1s ease
|
||||
|
||||
&:hover, &.hover // Add the hover class here to be triggered by mouseenter/mouseleave
|
||||
background-color: gray
|
||||
@@ -10,11 +10,17 @@
|
||||
{{ /if }}
|
||||
|
||||
{{ #if includeLayoutButton }}
|
||||
{{ >layoutButton }}
|
||||
<div class="btn-group">
|
||||
<button id="layout" type="button" class="btn btn-sm btn-default dropdown-toggle" data-container="body" data-toggle="dropdown" aria-expanded="false" data-placement="right" title="Layout" rel="tooltip">
|
||||
<span class="fa fa-th-large"></span>
|
||||
</button>
|
||||
{{ >layoutChooser }}
|
||||
</div>
|
||||
{{ /if }}
|
||||
|
||||
{{ #if includeHangingProtocolButtons }}
|
||||
{{ >hangingProtocolButtons }}
|
||||
{{ >matchedProtocols }}
|
||||
{{ /if }}
|
||||
|
||||
{{ #each btnGroup }}
|
||||
|
||||
@@ -152,7 +152,7 @@ Template.toolbar.onRendered(function() {
|
||||
var states = toolManager.getToolDefaultStates();
|
||||
var disabledToolButtons = states.disabledToolButtons;
|
||||
var allToolbarButtons = $('#toolbar').find('button');
|
||||
if (disabledToolButtons.length > 0) {
|
||||
if (disabledToolButtons && disabledToolButtons.length > 0) {
|
||||
for (var i = 0; i < allToolbarButtons.length; i++) {
|
||||
var toolbarButton = allToolbarButtons[i];
|
||||
$(toolbarButton).prop('disabled', false);
|
||||
@@ -191,7 +191,7 @@ Template.toolbar.helpers({
|
||||
return this.toolbarOptions.includeHangingProtocolButtons;
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
},
|
||||
btnGroup: function() {
|
||||
if (this.toolbarOptions && this.toolbarOptions.btnGroup !== undefined) {
|
||||
|
||||
+4
-3
@@ -22,7 +22,7 @@ updateOrientationMarkers = function(element) {
|
||||
|
||||
var markers = {
|
||||
top: oppositeColumnString,
|
||||
left: oppositeRowString,
|
||||
left: oppositeRowString
|
||||
};
|
||||
|
||||
// If any vertical or horizontal flips are applied, change the orientation strings ahead of
|
||||
@@ -30,12 +30,13 @@ updateOrientationMarkers = function(element) {
|
||||
if (viewport.vflip) {
|
||||
markers.top = cornerstoneTools.orientation.invertOrientationString(markers.top);
|
||||
}
|
||||
|
||||
if (viewport.hflip) {
|
||||
markers.left = cornerstoneTools.orientation.invertOrientationString(markers.left);
|
||||
}
|
||||
|
||||
// Get the viewport orientation marker DOM elements
|
||||
var viewportOrientationMarkers = $(element).siblings(".viewportOrientationMarkers");
|
||||
var viewportOrientationMarkers = $(element).siblings('.viewportOrientationMarkers');
|
||||
var topMarker = viewportOrientationMarkers.find('.topMid');
|
||||
var leftMarker = viewportOrientationMarkers.find('.leftMid');
|
||||
|
||||
@@ -54,4 +55,4 @@ updateOrientationMarkers = function(element) {
|
||||
topMarker.text(markers.top);
|
||||
leftMarker.text(markers.left);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
<template name="viewportOverlay">
|
||||
<div class="imageViewerViewportOverlay noselect">
|
||||
{{ #unless tagDisplaySpecified }}
|
||||
<div class="topleft dicomTag">
|
||||
<div>{{formatPN patientName}}</div>
|
||||
<div>{{patientId}}</div>
|
||||
@@ -21,6 +22,47 @@
|
||||
<div>{{imageDimensions}}</div>
|
||||
<div>{{seriesDescription}}</div>
|
||||
</div>
|
||||
{{ /unless }}
|
||||
|
||||
{{ #if tagDisplayRightOnly }}
|
||||
<div class="topright dicomTag">
|
||||
<div>{{formatPN patientName}}</div>
|
||||
<div>{{patientId}}</div>
|
||||
<div class='priorIndicator'>{{prior}}</div>
|
||||
<div>{{studyDescription}}</div>
|
||||
<div>{{formatDA studyDate}} {{formatTM studyTime}}</div>
|
||||
</div>
|
||||
<div class="bottomright dicomTag">
|
||||
<div>Ser: {{seriesNumber}}</div>
|
||||
<div>Img: {{imageNumber}} ({{imageIndex}}/{{numImages}})</div>
|
||||
<div>{{#if frameRate}}{{frameRate}} FPS{{/if}}</div>
|
||||
<div>{{imageDimensions}}</div>
|
||||
<div>{{seriesDescription}}</div>
|
||||
<div>Zoom: {{formatNumberPrecision zoom 2}}%</div>
|
||||
<div>{{compression}}</div>
|
||||
<div>{{wwwc}}</div>
|
||||
</div>
|
||||
{{ /if }}
|
||||
|
||||
{{ #if tagDisplayLeftOnly }}
|
||||
<div class="topleft dicomTag">
|
||||
<div>{{formatPN patientName}}</div>
|
||||
<div>{{patientId}}</div>
|
||||
<div class='priorIndicator'>{{prior}}</div>
|
||||
<div>{{studyDescription}}</div>
|
||||
<div>{{formatDA studyDate}} {{formatTM studyTime}}</div>
|
||||
</div>
|
||||
<div class="bottomleft dicomTag">
|
||||
<div>Ser: {{seriesNumber}}</div>
|
||||
<div>Img: {{imageNumber}} ({{imageIndex}}/{{numImages}})</div>
|
||||
<div>{{#if frameRate}}{{frameRate}} FPS{{/if}}</div>
|
||||
<div>{{imageDimensions}}</div>
|
||||
<div>{{seriesDescription}}</div>
|
||||
<div>Zoom: {{formatNumberPrecision zoom 2}}%</div>
|
||||
<div>{{compression}}</div>
|
||||
<div>{{wwwc}}</div>
|
||||
</div>
|
||||
{{ /if }}
|
||||
{{>imageControls}}
|
||||
</div>
|
||||
</template>
|
||||
@@ -80,6 +80,20 @@ function getInstance(property) {
|
||||
return instance[property];
|
||||
}
|
||||
|
||||
function getTagDisplay(property) {
|
||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||
if (!this.imageId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var instance = cornerstoneTools.metaData.get('tagDisplay', this.imageId);
|
||||
if (!instance) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return instance[property];
|
||||
}
|
||||
|
||||
function getImage(viewportIndex) {
|
||||
var element = getElementIfNotEmpty(viewportIndex);
|
||||
if (!element) {
|
||||
@@ -172,6 +186,15 @@ Template.viewportOverlay.helpers({
|
||||
imageNumber: function() {
|
||||
return getInstance.call(this, 'instanceNumber');
|
||||
},
|
||||
tagDisplayLeftOnly: function() {
|
||||
return getTagDisplay.call(this, 'side') === 'L';
|
||||
},
|
||||
tagDisplayRightOnly: function() {
|
||||
return getTagDisplay.call(this, 'side') === 'R';
|
||||
},
|
||||
tagDisplaySpecified: function() {
|
||||
return getTagDisplay.call(this, 'side');
|
||||
},
|
||||
imageIndex: function() {
|
||||
return getInstance.call(this, 'index');
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.imageViewerViewportOverlay
|
||||
font-family: Helvetica, OpenSans
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif
|
||||
font-size: 13px
|
||||
color: #979797
|
||||
line-height: 18px
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
/*AccountsTemplates.configure({
|
||||
forbidClientAccountCreation: true // do not allow account creation
|
||||
});
|
||||
*/
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Converts the properties to URL query parameters. Based on:
|
||||
* http://stackoverflow.com/questions/111529/create-query-parameters-in-javascript
|
||||
*
|
||||
* @param data
|
||||
* @returns {string}
|
||||
*/
|
||||
encodeQueryData = function(data)
|
||||
{
|
||||
var ret = [];
|
||||
|
||||
for (var d in data) {
|
||||
if(data[d]) {
|
||||
ret.push(encodeURIComponent(d) + "=" + encodeURIComponent(data[d]));
|
||||
}
|
||||
}
|
||||
return ret.join("&");
|
||||
};
|
||||
@@ -1,115 +0,0 @@
|
||||
var exportFailed;
|
||||
|
||||
/**
|
||||
* Exports requested studies
|
||||
* @param studiesToExport Studies to export
|
||||
*/
|
||||
exportStudies = function(studiesToExport) {
|
||||
if (studiesToExport.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
queryStudies(studiesToExport, exportQueriedStudies);
|
||||
};
|
||||
|
||||
function exportQueriedStudies(studiesToExport) {
|
||||
var numberOfFilesToExport = 0;
|
||||
studiesToExport.forEach(function(study) {
|
||||
numberOfFilesToExport += getNumberOfFilesInStudy(study);
|
||||
});
|
||||
|
||||
progressDialog.show("Exporting Studies...", numberOfFilesToExport);
|
||||
|
||||
try {
|
||||
exportQueriedStudiesInternal(studiesToExport, numberOfFilesToExport);
|
||||
} catch (err) {
|
||||
progressDialog.close();
|
||||
console.error("Failed to export studies: " + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function exportQueriedStudiesInternal(studiesToExport, numberOfFilesToExport) {
|
||||
var zip = new JSZip();
|
||||
|
||||
exportFailed = false;
|
||||
var numberOfFilesExported = 0;
|
||||
|
||||
studiesToExport.forEach(function(study) {
|
||||
sortStudy(study);
|
||||
|
||||
var studyFolder = zip.folder(study.studyInstanceUid);
|
||||
|
||||
study.seriesList.forEach(function(series) {
|
||||
var seriesFolder = studyFolder.folder(series.seriesInstanceUid);
|
||||
|
||||
series.instances.forEach(function(instance) {
|
||||
if (!instance.wadouri) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If failed to download a dicom file, skip others
|
||||
if (exportFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Download and Zip the dicom file
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", instance.wadouri, true);
|
||||
xhr.responseType = "blob";
|
||||
|
||||
// Downloaded the dicom file completely
|
||||
xhr.onload = function(e) {
|
||||
// If failed to download a dicom file, skip others
|
||||
if (exportFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Failed to export a file
|
||||
if (xhr.readyState === 4 && xhr.status !== 200) {
|
||||
onExportFailed("File not downloaded: " + instance.wadouri);
|
||||
return;
|
||||
}
|
||||
|
||||
var blobFile = new Blob([xhr.response], {type: 'application/dicom'});
|
||||
|
||||
var fileReader = new FileReader();
|
||||
|
||||
fileReader.onload = function() {
|
||||
try {
|
||||
seriesFolder.file(instance.sopInstanceUid + ".dcm", fileReader.result, { binary: true });
|
||||
} catch(err) {
|
||||
onExportFailed(err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
numberOfFilesExported++;
|
||||
|
||||
if (numberOfFilesExported === numberOfFilesToExport) {
|
||||
var zipContent = zip.generate({ type: "blob" });
|
||||
saveAs(zipContent, "studies.zip");
|
||||
}
|
||||
|
||||
progressDialog.update(numberOfFilesExported);
|
||||
};
|
||||
|
||||
fileReader.readAsArrayBuffer(blobFile);
|
||||
};
|
||||
|
||||
// Failed to download the dicom file
|
||||
xhr.onerror = function() {
|
||||
onExportFailed("File not downloaded: " + instance.wadouri);
|
||||
};
|
||||
|
||||
xhr.send();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onExportFailed(err) {
|
||||
exportFailed = true;
|
||||
progressDialog.close();
|
||||
|
||||
//TODO: Export failed and dialog closed, so let user know
|
||||
console.error("Failed to export studies!", err);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* A global Blaze UI helper to capitalizes the first letter of an input String
|
||||
*
|
||||
* Credit to:
|
||||
*
|
||||
* http://stackoverflow.com/questions/1026069/capitalize-the-first-letter-of-string-in-javascript
|
||||
*/
|
||||
UI.registerHelper('capitalizeFirstLetter', function (context) {
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
return context.charAt(0).toUpperCase() + context.slice(1);
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* A global Blaze UI helper function to format JavaScript Dates using the Moment library
|
||||
*/
|
||||
UI.registerHelper('formatJSDate', function(context, format, options) {
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
var dateAsMoment = moment(new Date(context));
|
||||
var strFormat = 'MMM D, YYYY';
|
||||
if (options) {
|
||||
strFormat = format;
|
||||
}
|
||||
|
||||
return dateAsMoment.format(strFormat);
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Helper for retrieving username given userId
|
||||
*/
|
||||
UI.registerHelper('getUsername', function(userId) {
|
||||
var user = Meteor.users.findOne({
|
||||
userId: userId
|
||||
});
|
||||
|
||||
if (user && user.name) {
|
||||
return user.name;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Helper for checking datatype of a variable
|
||||
*/
|
||||
UI.registerHelper('ifTypeIs', function(value, match, attributeName) {
|
||||
if (typeof(value) === match) {
|
||||
return attributeName;
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
UI.registerHelper('inc', function(value) {
|
||||
return parseInt(value) + 1;
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* A global Blaze UI helper function to format JavaScript Dates using the Moment library
|
||||
*/
|
||||
UI.registerHelper('jsDateFromNow', function(context, format, options) {
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
Session.get('timeAgoVariable');
|
||||
|
||||
var dateAsMoment = moment(new Date(context));
|
||||
return dateAsMoment.fromNow();
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
UI.registerHelper('objectEach', function(object) {
|
||||
// http://stackoverflow.com/questions/30234732/how-to-print-key-and-values-in-meteor-template
|
||||
return _.map(object, function(value, key) {
|
||||
return _.extend({key: key}, value);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
UI.registerHelper('objectToPairs', function(object) {
|
||||
// http://stackoverflow.com/questions/30234732/how-to-print-key-and-values-in-meteor-template
|
||||
return _.map(object, function(value, key) {
|
||||
return {
|
||||
key: key,
|
||||
value: value
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* A global Blaze UI helper to Stringify a JavaScript object
|
||||
*
|
||||
* Credit to:
|
||||
*
|
||||
* http://stackoverflow.com/questions/1026069/capitalize-the-first-letter-of-string-in-javascript
|
||||
*/
|
||||
UI.registerHelper('prettyPrintStringify', function(context) {
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
var string = JSON.stringify(context, null, 2);
|
||||
string = string.replace(/['"]+/g, '');
|
||||
string = string.replace('{', '');
|
||||
string = string.replace('}', '');
|
||||
string = string.replace(',', '\n');
|
||||
return string;
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
var instanceClassViewportSettingsFunctions = {};
|
||||
getInstanceClassDefaultViewport = function(series, enabledElement, imageId) {
|
||||
var instanceClass = series.sopClassUid;
|
||||
|
||||
if (!instanceClassViewportSettingsFunctions[instanceClass]) {
|
||||
return;
|
||||
}
|
||||
|
||||
return instanceClassViewportSettingsFunctions[instanceClass](series, enabledElement, imageId);
|
||||
};
|
||||
|
||||
setInstanceClassDefaultViewportFunction = function(instanceClass, fn) {
|
||||
instanceClassViewportSettingsFunctions[instanceClass] = fn;
|
||||
};
|
||||
@@ -0,0 +1,211 @@
|
||||
// Displays Series in Viewports given a Protocol and list of Studies
|
||||
LayoutManager = class LayoutManager {
|
||||
constructor(parentNode, studies) {
|
||||
log.info('LayoutManager');
|
||||
|
||||
this.parentNode = parentNode;
|
||||
this.studies = studies;
|
||||
this.viewportData = [];
|
||||
this.layoutTemplateName = 'gridLayout';
|
||||
this.layoutProps = {
|
||||
rows: 1,
|
||||
columns: 1
|
||||
};
|
||||
|
||||
this.viewportData = [];
|
||||
|
||||
this.isZoomed = false;
|
||||
}
|
||||
|
||||
getNumberOfViewports() {
|
||||
return this.layoutProps.rows * this.layoutProps.columns;
|
||||
}
|
||||
|
||||
setDefaultViewportData() {
|
||||
var numViewports = this.getNumberOfViewports();
|
||||
|
||||
var viewportIndex = 0;
|
||||
var self = this;
|
||||
this.studies.forEach(function(study) {
|
||||
study.seriesList.forEach(function(series) {
|
||||
if (!series.instances.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var currentViewportData;
|
||||
var existingViewportData = self.viewportData[viewportIndex];
|
||||
if (self.viewportData[viewportIndex]) {
|
||||
currentViewportData = {
|
||||
viewportIndex: existingViewportData.viewportIndex,
|
||||
studyInstanceUid: existingViewportData.studyInstanceUid,
|
||||
seriesInstanceUid: existingViewportData.seriesInstanceUid,
|
||||
sopInstanceUid: existingViewportData.sopInstanceUid,
|
||||
viewport: existingViewportData.viewport,
|
||||
imageId: existingViewportData.imageId,
|
||||
currentImageIdIndex: existingViewportData.currentImageIdIndex // TODO Remove this once currentImageIdIndex is removed from imageViewerViewports
|
||||
};
|
||||
|
||||
self.viewportData[viewportIndex] = currentViewportData;
|
||||
} else {
|
||||
// This tests to make sure there is actually image data in this instance
|
||||
// TODO: Change this when we add PDF and MPEG support
|
||||
// See https://ohiforg.atlassian.net/browse/LT-227
|
||||
var firstInstance = series.instances[0];
|
||||
if (!firstInstance.rows || !firstInstance.columns) {
|
||||
currentViewportData = {};
|
||||
} else {
|
||||
currentViewportData = {
|
||||
viewportIndex: viewportIndex,
|
||||
studyInstanceUid: study.studyInstanceUid,
|
||||
seriesInstanceUid: series.seriesInstanceUid,
|
||||
sopInstanceUid: firstInstance.sopInstanceUid,
|
||||
currentImageIdIndex: 0 // TODO Remove this once currentImageIdIndex is removed from imageViewerViewports
|
||||
};
|
||||
}
|
||||
|
||||
self.viewportData.push(currentViewportData);
|
||||
}
|
||||
|
||||
viewportIndex++;
|
||||
|
||||
if (viewportIndex === numViewports) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (viewportIndex === numViewports) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateViewports() {
|
||||
log.info('updateViewports');
|
||||
|
||||
if (!this.viewportData ||
|
||||
!this.viewportData.length ||
|
||||
this.viewportData.length !== this.getNumberOfViewports()) {
|
||||
this.setDefaultViewportData();
|
||||
}
|
||||
|
||||
// imageViewerViewports occasionally needs relevant layout data in order to set
|
||||
// the element style of the viewport in question
|
||||
var layoutProps = this.layoutProps;
|
||||
var data = $.extend({
|
||||
viewportData: []
|
||||
}, layoutProps);
|
||||
|
||||
this.viewportData.forEach(function(viewportData) {
|
||||
var viewportDataAndLayoutProps = $.extend(viewportData, layoutProps);
|
||||
data.viewportData.push(viewportDataAndLayoutProps);
|
||||
});
|
||||
|
||||
var layoutTemplate = Template[this.layoutTemplateName];
|
||||
|
||||
$(this.parentNode).html('');
|
||||
Blaze.renderWithData(layoutTemplate, data, this.parentNode);
|
||||
|
||||
Session.set('LayoutManagerUpdated', Random.id());
|
||||
|
||||
this.isZoomed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function destroys and re-renders the imageViewerViewport template.
|
||||
* It uses the data provided (object containing seriesIndex and studyIndex) to
|
||||
* load a new series into the produced viewport.
|
||||
*
|
||||
* @param viewportIndex
|
||||
* @param data An object containing a seriesIndex and studyIndex for a study to load into this viewport
|
||||
*/
|
||||
rerenderViewportWithNewSeries(viewportIndex, data) {
|
||||
// The parent container is identified because it is later removed from the DOM
|
||||
var element = $('.imageViewerViewport').eq(viewportIndex);
|
||||
var container = element.parents('.viewportContainer').get(0);
|
||||
|
||||
// Record the current viewportIndex so this can be passed into the re-rendering call
|
||||
data.viewportIndex = viewportIndex;
|
||||
|
||||
// Update the dictionary of loaded series for the specified viewport
|
||||
this.viewportData[viewportIndex] = {
|
||||
viewportIndex: viewportIndex,
|
||||
seriesInstanceUid: data.seriesInstanceUid,
|
||||
studyInstanceUid: data.studyInstanceUid,
|
||||
renderedCallback: data.renderedCallback,
|
||||
currentImageIdIndex: 0
|
||||
};
|
||||
|
||||
// Remove the hover styling
|
||||
element.find('canvas').not('.magnifyTool').removeClass('faded');
|
||||
|
||||
// Remove the whole template, add in the new one
|
||||
var viewportContainer = element.parents('.removable');
|
||||
|
||||
var newViewportContainer = document.createElement('div');
|
||||
newViewportContainer.className = 'removable';
|
||||
|
||||
// Remove the parent element of the template
|
||||
// This is a workaround since otherwise Blaze UI onDestroyed doesn't fire
|
||||
viewportContainer.remove();
|
||||
|
||||
container.appendChild(newViewportContainer);
|
||||
|
||||
// Render and insert the template
|
||||
Blaze.renderWithData(Template.imageViewerViewport, data, newViewportContainer);
|
||||
|
||||
Session.set('LayoutManagerUpdated', Random.id());
|
||||
}
|
||||
|
||||
enlargeViewport(viewportIndex) {
|
||||
log.info('Zooming Into Viewport: ' + viewportIndex);
|
||||
|
||||
if (!this.viewportData ||
|
||||
!this.viewportData.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clone the array for later
|
||||
this.previousViewportData = this.viewportData.slice(0);
|
||||
|
||||
var singleViewportData = $.extend({}, this.viewportData[viewportIndex]);
|
||||
singleViewportData.rows = 1;
|
||||
singleViewportData.columns = 1;
|
||||
singleViewportData.viewportIndex = 0;
|
||||
|
||||
var data = {
|
||||
viewportData: [singleViewportData],
|
||||
rows: 1,
|
||||
columns: 1
|
||||
};
|
||||
|
||||
var layoutTemplate = Template['gridLayout'];
|
||||
|
||||
$(this.parentNode).html('');
|
||||
Blaze.renderWithData(layoutTemplate, data, this.parentNode);
|
||||
|
||||
this.isZoomed = true;
|
||||
this.zoomedViewportIndex = viewportIndex;
|
||||
this.viewportData = data.viewportData;
|
||||
|
||||
Session.set('LayoutManagerUpdated', Random.id());
|
||||
}
|
||||
|
||||
resetPreviousLayout() {
|
||||
if (!this.isZoomed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.previousViewportData[this.zoomedViewportIndex] = $.extend({}, this.viewportData[0]);
|
||||
this.previousViewportData[this.zoomedViewportIndex].viewportIndex = this.zoomedViewportIndex;
|
||||
this.viewportData = this.previousViewportData;
|
||||
this.updateViewports();
|
||||
}
|
||||
|
||||
toggleEnlargement(viewportIndex) {
|
||||
if (this.isZoomed) {
|
||||
this.resetPreviousLayout();
|
||||
} else {
|
||||
this.enlargeViewport(viewportIndex);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -89,6 +89,21 @@ addMetaData = function(imageId, data) {
|
||||
metaDataLookup[imageId] = metaData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a set of metaData to the Cornerstone metaData provider given a specific
|
||||
* imageId, type, and dataset
|
||||
*
|
||||
* @param imageId
|
||||
* @param type (e.g. series, instance, tagDisplay)
|
||||
* @param data
|
||||
*/
|
||||
addSpecificMetadata = function(imageId, type, data) {
|
||||
var metaData = {};
|
||||
metaData[type] = data;
|
||||
|
||||
metaDataLookup[imageId] = $.extend(metaDataLookup[imageId], metaData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Looks up metaData for Cornerstone Tools given a specified type and imageId
|
||||
* A type may be, e.g. 'study', or 'patient', or 'imagePlane'. These types
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* This function destroys and re-renders the imageViewerViewport template.
|
||||
* It uses the data provided (object containing seriesIndex and studyIndex) to
|
||||
* load a new series into the produced viewport.
|
||||
*
|
||||
* @param element
|
||||
* @param data An object containing a seriesIndex and studyIndex for a study to load into this viewport
|
||||
* @function renderedCallback An optional callback to be executed after the new template is rendered
|
||||
*/
|
||||
rerenderViewportWithNewSeries = function(element, data, renderedCallback) {
|
||||
// Get the container and index of the current viewport.
|
||||
// The parent container is identified because it is later removed from the DOM
|
||||
var container = $(element).parents('.viewportContainer').get(0);
|
||||
var viewportIndex = $(container).index();
|
||||
|
||||
// Record the current viewportIndex so this can be passed into the re-rendering call
|
||||
data.viewportIndex = viewportIndex;
|
||||
|
||||
// Update the dictionary of loaded series for the specified viewport
|
||||
OHIF.viewer.loadedSeriesData[viewportIndex] = {
|
||||
seriesInstanceUid: data.seriesInstanceUid,
|
||||
studyInstanceUid: data.studyInstanceUid,
|
||||
currentImageIdIndex: 0
|
||||
};
|
||||
|
||||
// Remove the hover styling
|
||||
$(element).find('canvas').not('.magnifyTool').removeClass("faded");
|
||||
|
||||
// Remove the whole template, add in the new one
|
||||
var viewportContainer = $(element).parents('.removable');
|
||||
|
||||
var newViewportContainer = document.createElement('div');
|
||||
newViewportContainer.className = 'removable';
|
||||
|
||||
// Remove the parent element of the template
|
||||
// This is a workaround since otherwise Blaze UI onDestroyed doesn't fire
|
||||
viewportContainer.remove();
|
||||
|
||||
container.appendChild(newViewportContainer);
|
||||
|
||||
// Pass the renderedCallback to the imageViewerViewport template
|
||||
if (renderedCallback && typeof renderedCallback === "function") {
|
||||
data.renderedCallback = renderedCallback;
|
||||
}
|
||||
|
||||
// Render and insert the template
|
||||
UI.renderWithData(Template.imageViewerViewport, data, newViewportContainer);
|
||||
};
|
||||
@@ -15,8 +15,22 @@ resizeViewportElements = function() {
|
||||
viewportResizeTimer = setTimeout(function() {
|
||||
var elements = $('.imageViewerViewport').not('.empty');
|
||||
elements.each(function(index, element) {
|
||||
var enabledElement;
|
||||
try {
|
||||
enabledElement = cornerstone.getEnabledElement(element);
|
||||
} catch(error) {
|
||||
return;
|
||||
}
|
||||
|
||||
cornerstone.resize(element, true);
|
||||
|
||||
if (enabledElement.fitToWindow === false) {
|
||||
var imageId = enabledElement.image.imageId;
|
||||
var instance = cornerstoneTools.metaData.get('instance', imageId);
|
||||
var instanceClassViewport = getInstanceClassDefaultViewport(instance, enabledElement, imageId);
|
||||
cornerstone.setViewport(element, instanceClassViewport);
|
||||
}
|
||||
|
||||
// TODO= Refactor this into separate scrollbar resize function
|
||||
var currentOverlay = $(element).siblings('.imageViewerViewportOverlay');
|
||||
var imageControls = currentOverlay.find('.imageControls');
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
setMammogramViewportAlignment = function(series, enabledElement, imageId) {
|
||||
// Don't apply the MG viewport alignment to other series types
|
||||
var viewTypes = ['MLO', 'CC', 'LM', 'ML', 'XCCL'];
|
||||
var requiresInversion = ['LM']; // Tomo series are flipped
|
||||
|
||||
var instance = cornerstoneTools.metaData.get('instance', imageId);
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
var laterality = instance.laterality;
|
||||
var element = enabledElement.element;
|
||||
var position;
|
||||
|
||||
var left = $(enabledElement.canvas).offset().left;
|
||||
var right = left + enabledElement.canvas.width;
|
||||
|
||||
if (viewTypes.indexOf(instance.viewPosition) < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we should flip the laterality
|
||||
if (requiresInversion.indexOf(instance.viewPosition) > -1) {
|
||||
if (laterality === "R") {
|
||||
laterality = "L";
|
||||
} else if (laterality === "L") {
|
||||
laterality = "R";
|
||||
}
|
||||
}
|
||||
|
||||
if (laterality === "R") {
|
||||
// Set X translation to Canvas max in image pixels - image width
|
||||
// This places it on the right side of the screen
|
||||
position = cornerstone.pageToPixel(element, right, 0);
|
||||
if (position.x !== enabledElement.image.width) {
|
||||
enabledElement.viewport.translation.x += position.x - enabledElement.image.width;
|
||||
}
|
||||
|
||||
addSpecificMetadata(imageId, 'tagDisplay', {
|
||||
side: 'L'
|
||||
});
|
||||
|
||||
} else if (laterality === 'L') {
|
||||
// Use pageToPixel to and jQuery to find pixel coordinates of leftmost
|
||||
// side of the current canvas
|
||||
position = cornerstone.pageToPixel(element, left, 0);
|
||||
if (position.x !== 0) {
|
||||
enabledElement.viewport.translation.x += position.x;
|
||||
}
|
||||
|
||||
addSpecificMetadata(imageId, 'tagDisplay', {
|
||||
side: 'R'
|
||||
});
|
||||
}
|
||||
|
||||
return enabledElement.viewport;
|
||||
};
|
||||
|
||||
Meteor.startup(function() {
|
||||
setInstanceClassDefaultViewportFunction("1.2.840.10008.5.1.4.1.1.1.2", setMammogramViewportAlignment);
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
validate.validators.equals = function(value, options, key, attributes) {
|
||||
if (options && value !== options.value) {
|
||||
return key + 'must equal ' + options.value;
|
||||
}
|
||||
};
|
||||
|
||||
validate.validators.doesNotEqual = function(value, options, key) {
|
||||
if (options && value === options.value) {
|
||||
return key + 'cannot equal ' + options.value;
|
||||
}
|
||||
};
|
||||
|
||||
validate.validators.contains = function(value, options, key) {
|
||||
if (options && value.indexOf && value.indexOf(options.value) === -1) {
|
||||
return key + 'must contain ' + options.value;
|
||||
}
|
||||
};
|
||||
|
||||
validate.validators.doesNotContain = function(value, options, key) {
|
||||
if (options && value.indexOf && value.indexOf(options.value) !== -1) {
|
||||
return key + 'cannot contain ' + options.value;
|
||||
}
|
||||
};
|
||||
@@ -1,656 +0,0 @@
|
||||
/**
|
||||
* This is a temporary function which will return a hardcoded hanging protocol as a JavaScript object
|
||||
*/
|
||||
function getMammoHangingProtocolObject() {
|
||||
|
||||
var protocol = [{
|
||||
stage: 1,
|
||||
rows: 2,
|
||||
columns: 4,
|
||||
viewports: [{
|
||||
seriesDescription: 'RCC',
|
||||
study: 'prior'
|
||||
}, {
|
||||
seriesDescription: 'LCC',
|
||||
study: 'prior'
|
||||
}, {
|
||||
seriesDescription: 'RMLO',
|
||||
study: 'prior'
|
||||
}, {
|
||||
seriesDescription: 'LMLO',
|
||||
study: 'prior'
|
||||
}, {
|
||||
seriesDescription: 'RCC',
|
||||
study: 'current'
|
||||
}, {
|
||||
seriesDescription: 'LCC',
|
||||
study: 'current'
|
||||
}, {
|
||||
seriesDescription: 'RMLO',
|
||||
study: 'current'
|
||||
}, {
|
||||
seriesDescription: 'LMLO',
|
||||
study: 'current'
|
||||
}
|
||||
]
|
||||
}, {
|
||||
stage: 2,
|
||||
rows: 1,
|
||||
columns: 2,
|
||||
viewports: [{
|
||||
seriesDescription: 'RCC',
|
||||
study: 'current'
|
||||
}, {
|
||||
seriesDescription: 'LCC',
|
||||
study: 'current'
|
||||
}
|
||||
]
|
||||
}, {
|
||||
stage: 3,
|
||||
rows: 1,
|
||||
columns: 2,
|
||||
viewports: [{
|
||||
seriesDescription: 'RMLO',
|
||||
study: 'current'
|
||||
}, {
|
||||
seriesDescription: 'LMLO',
|
||||
study: 'current'
|
||||
}
|
||||
]
|
||||
}, {
|
||||
stage: 4,
|
||||
rows: 1,
|
||||
columns: 2,
|
||||
viewports: [{
|
||||
seriesDescription: 'RCC',
|
||||
study: 'current'
|
||||
}, {
|
||||
seriesDescription: 'RCC',
|
||||
study: 'prior'
|
||||
}
|
||||
]
|
||||
}, {
|
||||
stage: 5,
|
||||
rows: 1,
|
||||
columns: 2,
|
||||
viewports: [{
|
||||
seriesDescription: 'LCC',
|
||||
study: 'current'
|
||||
}, {
|
||||
seriesDescription: 'LCC',
|
||||
study: 'prior'
|
||||
}
|
||||
]
|
||||
}, {
|
||||
stage: 6,
|
||||
rows: 1,
|
||||
columns: 2,
|
||||
viewports: [{
|
||||
seriesDescription: 'LMLO',
|
||||
study: 'current'
|
||||
}, {
|
||||
seriesDescription: 'LMLO',
|
||||
study: 'prior'
|
||||
}
|
||||
]
|
||||
}, {
|
||||
stage: 7,
|
||||
rows: 1,
|
||||
columns: 2,
|
||||
viewports: [{
|
||||
seriesDescription: 'RMLO',
|
||||
study: 'current'
|
||||
}, {
|
||||
seriesDescription: 'RMLO',
|
||||
study: 'prior'
|
||||
}
|
||||
]
|
||||
}, {
|
||||
stage: 8,
|
||||
rows: 2,
|
||||
columns: 4,
|
||||
viewports: [{
|
||||
seriesDescription: 'RCC',
|
||||
study: 'prior',
|
||||
options: {
|
||||
includeCADMarkers: true
|
||||
}
|
||||
}, {
|
||||
seriesDescription: 'LCC',
|
||||
study: 'prior',
|
||||
options: {
|
||||
includeCADMarkers: true
|
||||
}
|
||||
}, {
|
||||
seriesDescription: 'RMLO',
|
||||
study: 'prior',
|
||||
options: {
|
||||
includeCADMarkers: true
|
||||
}
|
||||
}, {
|
||||
seriesDescription: 'LMLO',
|
||||
study: 'prior',
|
||||
options: {
|
||||
includeCADMarkers: true
|
||||
}
|
||||
}, {
|
||||
seriesDescription: 'RCC',
|
||||
study: 'current',
|
||||
options: {
|
||||
includeCADMarkers: true
|
||||
}
|
||||
}, {
|
||||
seriesDescription: 'LCC',
|
||||
study: 'current',
|
||||
options: {
|
||||
includeCADMarkers: true
|
||||
}
|
||||
}, {
|
||||
seriesDescription: 'RMLO',
|
||||
study: 'current',
|
||||
options: {
|
||||
includeCADMarkers: true
|
||||
}
|
||||
}, {
|
||||
seriesDescription: 'LMLO',
|
||||
study: 'current',
|
||||
options: {
|
||||
includeCADMarkers: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return protocol;
|
||||
}
|
||||
|
||||
function findSeriesByDescription(seriesDescription, study) {
|
||||
var seriesInstanceUid;
|
||||
study.seriesList.forEach(function(series) {
|
||||
if (!series.seriesDescription) {
|
||||
return;
|
||||
}
|
||||
|
||||
var currentSeriesDescription = series.seriesDescription.replace(' ', '');
|
||||
if (currentSeriesDescription === seriesDescription) {
|
||||
seriesInstanceUid = series.seriesInstanceUid;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return seriesInstanceUid;
|
||||
}
|
||||
|
||||
/**
|
||||
* (Work in progress) Uses the information from a DICOM Hanging Protocol
|
||||
* to identify and display studies and series in the image viewer
|
||||
*
|
||||
* @param hangingProtocol
|
||||
* @param inputData
|
||||
* @returns {Array} Array of viewport data to be displayed
|
||||
*/
|
||||
function applyHangingProtocol(hangingProtocol, inputData) {
|
||||
var presentationGroup = inputData.DisplaySetPresentationGroup || 1;
|
||||
var studies = ViewerStudies;
|
||||
var currentProtocolData = hangingProtocol[presentationGroup - 1];
|
||||
|
||||
var viewportData = {
|
||||
viewports: [],
|
||||
viewportRows: currentProtocolData.rows,
|
||||
viewportColumns: currentProtocolData.columns
|
||||
};
|
||||
|
||||
var currentStudy = ViewerStudies.find({}, {$sort: {studyDate: 1}}).fetch()[0];
|
||||
var otherStudies = WorklistStudies.find({
|
||||
patientId: currentStudy.patientId,
|
||||
studyInstanceUid: {
|
||||
$ne: currentStudy.studyInstanceUid
|
||||
}
|
||||
}, {$sort: {
|
||||
studyDate: 1
|
||||
}}).fetch();
|
||||
var priorStudy = otherStudies[0];
|
||||
|
||||
currentProtocolData.viewports.forEach(function(viewport, index) {
|
||||
if (viewport.study === 'current') {
|
||||
study = currentStudy;
|
||||
} else if (viewport.study === 'prior') {
|
||||
study = currentStudy;
|
||||
/*ViewerStudies.find({
|
||||
studyInstanceUid: {
|
||||
$ne: currentStudy.studyInstanceUid
|
||||
}
|
||||
}, {$sort: {studyDate: 1}}).fetch()[0];*/
|
||||
}
|
||||
|
||||
seriesInstanceUid = findSeriesByDescription(viewport.seriesDescription, study);
|
||||
|
||||
viewportData.viewports[index] = {
|
||||
seriesInstanceUid: seriesInstanceUid,
|
||||
studyInstanceUid: study.studyInstanceUid,
|
||||
currentImageIdIndex: 0,
|
||||
options: viewport.options
|
||||
};
|
||||
});
|
||||
|
||||
return viewportData;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This is a temporary function which will return a hardcoded hanging protocol as a JavaScript object
|
||||
* The purpose of this is to act as a stub until we are actually parsing DICOM Hanging Protocol files,
|
||||
* since Orthanc doesn't seem to support them yet.
|
||||
*
|
||||
* Soon we will be using instanceDataToJsObject to produce this object from the HangingProtocol WADO
|
||||
* instance
|
||||
*
|
||||
*/
|
||||
function getMammoHangingProtocol() {
|
||||
var tagValues = {
|
||||
HangingProtocolStorage: '1.2.840.10008.5.1.4.38.1'
|
||||
};
|
||||
|
||||
var protocol = {
|
||||
sopClassUid: tagValues.HangingProtocolStorage,
|
||||
sopInstanceUid: '1.2.840.113986.2.664566.21121125.85669.911', // A random Uid
|
||||
hangingProtocolName: 'MammoCadProtocol',
|
||||
hangingProtocolDescription: 'Mammography screening protocol',
|
||||
hangingProtocolLevel: 'SITE',
|
||||
hangingProtocolCreator: 'Erik',
|
||||
hangingProtocolCreationDateTime: '20020101104200',
|
||||
hangingProtocolDefinitionSequence: [{
|
||||
modality: 'MG',
|
||||
laterality: '',
|
||||
procedureCodeSequence: {
|
||||
codeValue: 98765,
|
||||
codingSchemeDesignator: '99Local',
|
||||
codingSchemeVersion: 1.5,
|
||||
codeMeaning: 'Mammogram'
|
||||
},
|
||||
anatomicRegionSequence: {
|
||||
codeValue: 'T-D1100',
|
||||
codingSchemeDesignator: 'SNM3',
|
||||
codeMeaning: 'BREAST'
|
||||
},
|
||||
reasonForRequestedProcedureCodeSequence: {
|
||||
codeValue: 'I67.1',
|
||||
codingSchemeDesignator: 'I10',
|
||||
codeMeaning: 'Calcification'
|
||||
}
|
||||
}],
|
||||
hangingProtocolUserIdentificationCodeSequence: [],
|
||||
hangingProtocolUserGroupName: 'ABC Hospital',
|
||||
numberOfPriorsReferenced: 1,
|
||||
imageSetsSequence: [{
|
||||
imageSetSelectorSequence: [{
|
||||
ImageSetSelectorUsageFlag: 'NO_MATCH',
|
||||
SelectorAttribute: '00180015',
|
||||
SelectorValueNumber: '1',
|
||||
SelectorAttributeVR: 'CS',
|
||||
SelectorCSValue: 'BREAST'
|
||||
}],
|
||||
timeBasedImageSetsSequence: [{
|
||||
ImageSetNumber: 1,
|
||||
ImageSetSelectorCategory: 'RELATIVE_TIME',
|
||||
RelativeTime: '0\0',
|
||||
RelativeTimeUnits: 'MINUTES',
|
||||
ImageSetLabel: 'Current MG Breast'
|
||||
},
|
||||
{
|
||||
ImageSetNumber: 2,
|
||||
ImageSetSelectorCategory: 'ABSTRACT_PRIOR',
|
||||
AbstractPriorValue: '1\\1',
|
||||
ImageSetLabel: 'Prior MG Breast'
|
||||
}]
|
||||
}],
|
||||
// Skip Number of Screens for now,
|
||||
// Skip Nominal Screen Definition Sequence for now,
|
||||
|
||||
// http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.23.3.html
|
||||
DisplaySetsSequence: [
|
||||
{
|
||||
// Left side image (R MLO Current)
|
||||
ImageSetNumber: 1,
|
||||
DisplaySetNumber: 1,
|
||||
DisplaySetPresentationGroup: 1,
|
||||
DisplaySetPresentationGroupDescription: 'Current Mediolateral only',
|
||||
ImageBoxesSequence: [{
|
||||
DisplayEnvironmentSpatialPosition: '0\0.2\0.16667\0',
|
||||
ImageBoxNumber: 1,
|
||||
ImageBoxLayoutType: 'STACK'
|
||||
}],
|
||||
FilterOperationsSequence: [{
|
||||
SelectorAttributeVR: 'CS',
|
||||
SelectorCSValue: 'R CC',
|
||||
FilterByCategory: 'SERIES_DESCRIPTION',
|
||||
FilterByOperator: 'MEMBER_OF'
|
||||
}]
|
||||
}, {
|
||||
// Right side image (L MLO Current)
|
||||
ImageSetNumber: 1,
|
||||
DisplaySetNumber: 1,
|
||||
DisplaySetPresentationGroup: 1,
|
||||
ImageBoxesSequence: [{
|
||||
ImageBoxNumber: 1,
|
||||
ImageBoxLayoutType: 'STACK'
|
||||
}],
|
||||
FilterOperationsSequence: [{
|
||||
SelectorAttributeVR: 'CS',
|
||||
SelectorCSValue: 'L CC',
|
||||
FilterByCategory: 'SERIES_DESCRIPTION',
|
||||
FilterByOperator: 'MEMBER_OF'
|
||||
}]
|
||||
},
|
||||
{
|
||||
// Left side image (R MLO Current)
|
||||
ImageSetNumber: 1,
|
||||
DisplaySetNumber: 1,
|
||||
DisplaySetPresentationGroup: 2,
|
||||
DisplaySetPresentationGroupDescription: 'Current Mediolateral only',
|
||||
ImageBoxesSequence: [{
|
||||
DisplayEnvironmentSpatialPosition: '0\0.2\0.16667\0',
|
||||
ImageBoxNumber: 1,
|
||||
ImageBoxLayoutType: 'STACK'
|
||||
}],
|
||||
FilterOperationsSequence: [{
|
||||
SelectorAttributeVR: 'CS',
|
||||
SelectorCSValue: 'R MLO',
|
||||
FilterByCategory: 'SERIES_DESCRIPTION',
|
||||
FilterByOperator: 'MEMBER_OF'
|
||||
}],
|
||||
ReformattingOperationType: '',
|
||||
ReformattingThickness: '',
|
||||
ReformattingInterval: '',
|
||||
ReformattingOperationInitialViewDirection: '',
|
||||
SortingOperationsSequence: [],
|
||||
DisplaySetPatientOrientation: '',
|
||||
VOIType: ''
|
||||
}, {
|
||||
// Right side image (L MLO Current)
|
||||
ImageSetNumber: 1,
|
||||
DisplaySetNumber: 1,
|
||||
DisplaySetPresentationGroup: 2,
|
||||
ImageBoxesSequence: [{
|
||||
ImageBoxNumber: 1,
|
||||
ImageBoxLayoutType: 'STACK'
|
||||
}],
|
||||
FilterOperationsSequence: [{
|
||||
SelectorAttributeVR: 'CS',
|
||||
SelectorCSValue: 'L MLO',
|
||||
FilterByCategory: 'SERIES_DESCRIPTION',
|
||||
FilterByOperator: 'MEMBER_OF'
|
||||
}]
|
||||
}, {
|
||||
// Testing purposes
|
||||
// Left side image (R MLO Current)
|
||||
ImageSetNumber: 1,
|
||||
DisplaySetNumber: 1,
|
||||
DisplaySetPresentationGroup: 3,
|
||||
DisplaySetPresentationGroupDescription: 'Current Mediolateral only',
|
||||
ImageBoxesSequence: [{
|
||||
DisplayEnvironmentSpatialPosition: '0\0.2\0.16667\0',
|
||||
ImageBoxNumber: 1,
|
||||
ImageBoxLayoutType: 'STACK'
|
||||
}],
|
||||
FilterOperationsSequence: [{
|
||||
SelectorAttributeVR: 'CS',
|
||||
SelectorCSValue: 'R MLO',
|
||||
FilterByCategory: 'SERIES_DESCRIPTION',
|
||||
FilterByOperator: 'MEMBER_OF'
|
||||
}]
|
||||
}, {
|
||||
// Right side image (R CC Current)
|
||||
ImageSetNumber: 1,
|
||||
DisplaySetNumber: 1,
|
||||
DisplaySetPresentationGroup: 3,
|
||||
ImageBoxesSequence: [{
|
||||
ImageBoxNumber: 1,
|
||||
ImageBoxLayoutType: 'STACK'
|
||||
}],
|
||||
FilterOperationsSequence: [{
|
||||
SelectorAttributeVR: 'CS',
|
||||
SelectorCSValue: 'R CC',
|
||||
FilterByCategory: 'SERIES_DESCRIPTION',
|
||||
FilterByOperator: 'MEMBER_OF'
|
||||
}]
|
||||
}],
|
||||
PartialDataDisplayHandling: 'MAINTAIN_LAYOUT',
|
||||
SynchronizedScrollingSequence: [],
|
||||
NavigationIndicatorSequence: []
|
||||
};
|
||||
|
||||
return protocol;
|
||||
}
|
||||
|
||||
function findSetsByPresentationGroup(DisplaySetsSequence, DisplaySetPresentationGroup) {
|
||||
var presentationGroupSets = [];
|
||||
DisplaySetsSequence.forEach(function(displaySet) {
|
||||
if (displaySet.DisplaySetPresentationGroup === DisplaySetPresentationGroup) {
|
||||
presentationGroupSets.push(displaySet);
|
||||
}
|
||||
});
|
||||
return presentationGroupSets;
|
||||
}
|
||||
|
||||
function findStudy(displaySet, studies) {
|
||||
// Placeholder for now
|
||||
return studies.find().fetch()[0].studyInstanceUid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the FilterOperationsSequence information to find
|
||||
* the relevant series for display in this display set
|
||||
*
|
||||
* @param displaySet
|
||||
* @param study
|
||||
* @returns {String} The instance Uid of the series to be displayed
|
||||
*/
|
||||
function findSeries(displaySet, study) {
|
||||
var categoryDictionary = {
|
||||
SERIES_DESCRIPTION: 'seriesDescription'
|
||||
};
|
||||
|
||||
// TODO=Put these inside a loop? We need to support multiple filter operations,
|
||||
// not just one
|
||||
var filterBy = displaySet.FilterOperationsSequence[0].FilterByCategory;
|
||||
var selectorValue = displaySet.FilterOperationsSequence[0].SelectorCSValue;
|
||||
var filterType = displaySet.FilterOperationsSequence[0].FilterByOperator;
|
||||
var category = categoryDictionary[filterBy];
|
||||
|
||||
var seriesInstanceUid;
|
||||
study.seriesList.forEach(function(series) {
|
||||
// TODO = Add other FilterBy operators
|
||||
if (filterType === 'MEMBER_OF') {
|
||||
if (series[category] === selectorValue) {
|
||||
seriesInstanceUid = series.seriesInstanceUid;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return seriesInstanceUid;
|
||||
}
|
||||
|
||||
/**
|
||||
* (Work in progress) Uses the information from a DICOM Hanging Protocol
|
||||
* to identify and display studies and series in the image viewer
|
||||
*
|
||||
* @param hangingProtocol
|
||||
* @param inputData
|
||||
* @returns {Array} Array of viewport data to be displayed
|
||||
*/
|
||||
function applyDICOMHangingProtocol(hangingProtocol, inputData) {
|
||||
log.info('applyDICOMHangingProtocol');
|
||||
var presentationGroup = inputData.DisplaySetPresentationGroup || 1;
|
||||
var studies = ViewerStudies;
|
||||
var currentDisplaySets = findSetsByPresentationGroup(hangingProtocol.DisplaySetsSequence, presentationGroup);
|
||||
|
||||
var viewportData = {
|
||||
viewports: []
|
||||
};
|
||||
|
||||
currentDisplaySets.forEach(function(displaySet, index) {
|
||||
// TODO= Find study information by image set properties and
|
||||
// image set number of this display set
|
||||
var ImageSetNumber = displaySet.ImageSetNumber;
|
||||
|
||||
studyInstanceUid = findStudy(displaySet, studies);
|
||||
|
||||
var study = ViewerStudies.findOne({studyInstanceUid: studyInstanceUid});
|
||||
seriesInstanceUid = findSeries(displaySet, study);
|
||||
|
||||
viewportData.viewports[index] = {
|
||||
seriesInstanceUid: seriesInstanceUid,
|
||||
studyInstanceUid: studyInstanceUid,
|
||||
currentImageIdIndex: 0
|
||||
};
|
||||
});
|
||||
|
||||
return viewportData;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an example of a hanging protocol
|
||||
* It takes in a set of studies, as well as the
|
||||
* number of rows and columns in the layout.
|
||||
*
|
||||
* It returns an array of objects, one for each viewport, detailing
|
||||
* which series should be loaded in the viewport.
|
||||
*
|
||||
* @param inputData
|
||||
* @returns {Array}
|
||||
*/
|
||||
function defaultHangingProtocol(inputData) {
|
||||
var studies = ViewerStudies.find().fetch();
|
||||
var viewportRows = inputData.viewportRows;
|
||||
var viewportColumns = inputData.viewportColumns;
|
||||
|
||||
// This is the most basic hanging protocol.
|
||||
var stacks = [];
|
||||
studies.forEach(function(study) {
|
||||
study.seriesList.forEach(function(series) {
|
||||
|
||||
// Ensure that the series has image data
|
||||
// (All images have rows)
|
||||
var anInstance = series.instances[0];
|
||||
if (!anInstance || !anInstance.rows) {
|
||||
return;
|
||||
}
|
||||
|
||||
var stack = {
|
||||
series: series,
|
||||
study: study
|
||||
};
|
||||
stacks.push(stack);
|
||||
});
|
||||
});
|
||||
|
||||
var viewportData = {
|
||||
viewports: []
|
||||
};
|
||||
|
||||
var numViewports = viewportRows * viewportColumns;
|
||||
for (var i=0; i < numViewports; ++i) {
|
||||
if (i >= stacks.length) {
|
||||
// We don't have enough stacks to fill the desired number of viewports, so stop here
|
||||
break;
|
||||
}
|
||||
viewportData.viewports[i] = {
|
||||
seriesInstanceUid: stacks[i].series.seriesInstanceUid,
|
||||
studyInstanceUid: stacks[i].study.studyInstanceUid,
|
||||
currentImageIdIndex: 0
|
||||
};
|
||||
}
|
||||
return viewportData;
|
||||
}
|
||||
|
||||
var hangingProtocol;
|
||||
var presentationGroup = 1;
|
||||
|
||||
function getHangingProtocol(inputData) {
|
||||
// TODO = Update this to use Collection logic
|
||||
var studies = ViewerStudies.find().fetch();
|
||||
|
||||
if (!studies.length) {
|
||||
log.warn("No studies provided to Hanging Protocol");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the unique modalities in this study
|
||||
var modalities = [];
|
||||
studies.forEach(function(study) {
|
||||
study.seriesList.forEach(function(series) {
|
||||
if (modalities.indexOf(series.modality) < 0) {
|
||||
modalities.push(series.modality);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (modalities.indexOf('MG') > -1) {
|
||||
var hp = getMammoHangingProtocolObject();
|
||||
Session.set('WindowManagerPresentationGroup', presentationGroup);
|
||||
var testData = applyHangingProtocol(hp, inputData);
|
||||
return testData;
|
||||
// Commenting this out for now so we can hardcode a MG protocol
|
||||
/*var hangingProtocol = getMammoHangingProtocol();
|
||||
Session.set('WindowManagerPresentationGroup', presentationGroup);
|
||||
return applyDICOMHangingProtocol(hangingProtocol, inputData);*/
|
||||
}
|
||||
|
||||
Session.set('WindowManagerPresentationGroup', undefined);
|
||||
return defaultHangingProtocol(inputData);
|
||||
}
|
||||
|
||||
function setHangingProtocol(protocol) {
|
||||
hangingProtocol = protocol;
|
||||
}
|
||||
|
||||
function previousPresentationGroup() {
|
||||
presentationGroup--;
|
||||
presentationGroup = Math.max(1, presentationGroup);
|
||||
|
||||
log.info('previousPresentationGroup: ' + presentationGroup);
|
||||
Session.set('WindowManagerPresentationGroup', presentationGroup);
|
||||
Session.set('UseHangingProtocol', Random.id());
|
||||
}
|
||||
|
||||
function nextPresentationGroup() {
|
||||
var numPresentationGroups = getNumPresentationGroups();
|
||||
|
||||
presentationGroup++;
|
||||
presentationGroup = Math.min(numPresentationGroups, presentationGroup);
|
||||
|
||||
log.info('nextPresentationGroup: ' + presentationGroup);
|
||||
Session.set('WindowManagerPresentationGroup', presentationGroup);
|
||||
Session.set('UseHangingProtocol', Random.id());
|
||||
}
|
||||
|
||||
function getNumPresentationGroups() {
|
||||
// TODO=Pull this information from the largest
|
||||
// value of DisplaySetPresentationGroup in the DisplaySetsSequence
|
||||
// after the DICOM-HP is parsed
|
||||
return 8;
|
||||
}
|
||||
|
||||
function getCurrentPresentationGroup() {
|
||||
return presentationGroup;
|
||||
}
|
||||
|
||||
function setCurrentPresentationGroup(groupNumber) {
|
||||
// Unused for now
|
||||
presentationGroup = groupNumber;
|
||||
}
|
||||
|
||||
WindowManager = {
|
||||
setHangingProtocol: setHangingProtocol,
|
||||
getHangingProtocol: getHangingProtocol,
|
||||
getNumPresentationGroups: getNumPresentationGroups,
|
||||
setCurrentPresentationGroup: setCurrentPresentationGroup,
|
||||
getCurrentPresentationGroup: getCurrentPresentationGroup,
|
||||
nextPresentationGroup: nextPresentationGroup,
|
||||
previousPresentationGroup: previousPresentationGroup
|
||||
};
|
||||
|
||||
WindowManager.setHangingProtocol(defaultHangingProtocol);
|
||||
@@ -5,64 +5,56 @@ Package.describe({
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
api.versionsFrom('1.2.0.2');
|
||||
api.versionsFrom('1.2.1');
|
||||
|
||||
api.use('standard-app-packages');
|
||||
api.use('ecmascript');
|
||||
api.use('http');
|
||||
api.use('jquery');
|
||||
api.use('stylus');
|
||||
api.use('accounts-password');
|
||||
api.use('ian:accounts-ui-bootstrap-3');
|
||||
api.use('http');
|
||||
api.use('practicalmeteor:loglevel');
|
||||
api.use('momentjs:moment');
|
||||
|
||||
// Our custom packages
|
||||
api.use('dicomweb');
|
||||
api.use('dimseservice');
|
||||
api.use('cornerstone');
|
||||
api.use('hangingprotocols');
|
||||
|
||||
api.addFiles('log.js', ['client', 'server']);
|
||||
api.addFiles('log.js');
|
||||
|
||||
// TODO= Find a meteor package for this
|
||||
// TODO: Use NPM depends for these
|
||||
api.addFiles('client/compatibility/jquery.hotkeys.js', 'client');
|
||||
|
||||
// Data validation (the Meteor package is currently out-of-date)
|
||||
api.addFiles('client/compatibility/validate.js', 'client');
|
||||
|
||||
// ---------- Collections ----------
|
||||
api.addFiles('client/collections.js', 'client');
|
||||
|
||||
// ---------- Components ----------
|
||||
|
||||
// Basic components
|
||||
api.addFiles('client/components/basic/layout/layout.html', 'client');
|
||||
api.addFiles('client/components/basic/layout/layout.styl', 'client');
|
||||
api.addFiles('client/components/basic/login/login.html', 'client');
|
||||
api.addFiles('client/components/basic/notFound/notFound.html', 'client');
|
||||
|
||||
api.addFiles('client/components/basic/loadingText/loadingText.html', 'client');
|
||||
api.addFiles('client/components/basic/loadingText/loadingText.styl', 'client');
|
||||
|
||||
api.addFiles('client/components/basic/studyNotFound/studyNotFound.html', 'client');
|
||||
api.addFiles('client/components/basic/studyNotFound/studyNotFound.styl', 'client');
|
||||
|
||||
api.addFiles('client/components/basic/removableBackdrop/removableBackdrop.html', 'client');
|
||||
api.addFiles('client/components/basic/removableBackdrop/removableBackdrop.styl', 'client');
|
||||
|
||||
api.addFiles('client/components/basic/hidingPanel/hidingPanel.html', 'client');
|
||||
api.addFiles('client/components/basic/hidingPanel/hidingPanel.js', 'client');
|
||||
api.addFiles('client/components/basic/hidingPanel/hidingPanel.styl', 'client');
|
||||
api.addFiles('client/components/basic/confirmDeleteDialog/confirmDeleteDialog.html', 'client');
|
||||
api.addFiles('client/components/basic/confirmDeleteDialog/confirmDeleteDialog.styl', 'client');
|
||||
api.addFiles('client/components/basic/confirmDeleteDialog/confirmDeleteDialog.js', 'client');
|
||||
|
||||
// Study Browser components
|
||||
api.addFiles('client/components/studyBrowser/studyBrowser/studyBrowser.html', 'client');
|
||||
api.addFiles('client/components/studyBrowser/studyBrowser/studyBrowser.js', 'client');
|
||||
api.addFiles('client/components/studyBrowser/studyBrowser/studyBrowser.styl', 'client');
|
||||
|
||||
api.addFiles('client/components/studyBrowser/relatedStudySelect/relatedStudySelect.html', 'client');
|
||||
api.addFiles('client/components/studyBrowser/relatedStudySelect/relatedStudySelect.js', 'client');
|
||||
api.addFiles('client/components/studyBrowser/relatedStudySelect/relatedStudySelect.styl', 'client');
|
||||
|
||||
api.addFiles('client/components/studyBrowser/thumbnailEntry/thumbnailEntry.html', 'client');
|
||||
api.addFiles('client/components/studyBrowser/thumbnailEntry/thumbnailEntry.js', 'client');
|
||||
api.addFiles('client/components/studyBrowser/thumbnailEntry/thumbnailEntry.styl', 'client');
|
||||
|
||||
api.addFiles('client/components/studyBrowser/thumbnails/thumbnails.html', 'client');
|
||||
api.addFiles('client/components/studyBrowser/thumbnails/thumbnails.js', 'client');
|
||||
|
||||
api.addFiles('client/components/studyBrowser/imageThumbnail/imageThumbnail.html', 'client');
|
||||
api.addFiles('client/components/studyBrowser/imageThumbnail/imageThumbnail.js', 'client');
|
||||
api.addFiles('client/components/studyBrowser/imageThumbnail/imageThumbnail.styl', 'client');
|
||||
@@ -72,9 +64,9 @@ Package.onUse(function(api) {
|
||||
api.addFiles('client/components/viewer/imageViewerViewport/imageViewerViewport.js', 'client');
|
||||
api.addFiles('client/components/viewer/imageViewerViewport/imageViewerViewport.styl', 'client');
|
||||
|
||||
api.addFiles('client/components/viewer/imageViewerViewports/imageViewerViewports.html', 'client');
|
||||
api.addFiles('client/components/viewer/imageViewerViewports/imageViewerViewports.js', 'client');
|
||||
api.addFiles('client/components/viewer/imageViewerViewports/imageViewerViewports.styl', 'client');
|
||||
api.addFiles('client/components/viewer/gridLayout/gridLayout.html', 'client');
|
||||
api.addFiles('client/components/viewer/gridLayout/gridLayout.js', 'client');
|
||||
api.addFiles('client/components/viewer/gridLayout/gridLayout.styl', 'client');
|
||||
|
||||
api.addFiles('client/components/viewer/loadingIndicator/loadingIndicator.html', 'client');
|
||||
api.addFiles('client/components/viewer/loadingIndicator/loadingIndicator.js', 'client');
|
||||
@@ -105,34 +97,22 @@ Package.onUse(function(api) {
|
||||
api.addFiles('client/components/viewer/playClipButton/playClipButton.html', 'client');
|
||||
api.addFiles('client/components/viewer/playClipButton/playClipButton.js', 'client');
|
||||
|
||||
api.addFiles('client/components/viewer/hangingProtocolButtons/hangingProtocolButtons.html', 'client');
|
||||
api.addFiles('client/components/viewer/hangingProtocolButtons/hangingProtocolButtons.js', 'client');
|
||||
|
||||
api.addFiles('client/components/viewer/layoutButton/layoutButton.html', 'client');
|
||||
|
||||
api.addFiles('client/components/viewer/toolbar/toolbar.html', 'client');
|
||||
api.addFiles('client/components/viewer/toolbar/toolbar.js', 'client');
|
||||
api.addFiles('client/components/viewer/toolbar/toolbar.styl', 'client');
|
||||
|
||||
api.addFiles('client/components/basic/progressDialog/progressDialog.html', 'client');
|
||||
api.addFiles('client/components/basic/progressDialog/progressDialog.styl', 'client');
|
||||
api.addFiles('client/components/basic/progressDialog/progressDialog.js', 'client');
|
||||
|
||||
api.addFiles('client/components/viewer/toolbarGroupButton/toolbarGroupButton.html', 'client');
|
||||
api.addFiles('client/components/viewer/toolbarGroupButton/toolbarGroupButton.styl', 'client');
|
||||
api.addFiles('client/components/viewer/toolbarGroupButton/toolbarGroupButton.js', 'client');
|
||||
|
||||
|
||||
// Library functions
|
||||
api.addFiles('lib/accountsConfig.js', 'client');
|
||||
api.addFiles('lib/layoutManager.js', 'client');
|
||||
api.addFiles('lib/createStacks.js', 'client');
|
||||
api.addFiles('lib/getImageId.js', 'client');
|
||||
api.addFiles('lib/getWADORSImageId.js', 'client');
|
||||
api.addFiles('lib/metaDataProvider.js', 'client');
|
||||
api.addFiles('lib/rerenderViewportWithNewSeries.js', 'client');
|
||||
api.addFiles('lib/sortStudy.js', 'client');
|
||||
api.addFiles('lib/toolManager.js', 'client');
|
||||
api.addFiles('lib/windowManager.js', 'client');
|
||||
api.addFiles('lib/enablePrefetchOnElement.js', 'client');
|
||||
api.addFiles('lib/displayReferenceLines.js', 'client');
|
||||
api.addFiles('lib/draggable.js', 'client');
|
||||
@@ -147,11 +127,11 @@ Package.onUse(function(api) {
|
||||
api.addFiles('lib/setFocusToActiveViewport.js', 'client');
|
||||
api.addFiles('lib/updateAllViewports.js', 'client');
|
||||
api.addFiles('lib/queryStudies.js', 'client');
|
||||
api.addFiles('lib/exportStudies.js', 'client');
|
||||
api.addFiles('lib/importStudies.js', 'client');
|
||||
api.addFiles('lib/encodeQueryData.js', 'server');
|
||||
api.addFiles('lib/validators.js', 'client');
|
||||
api.addFiles('lib/instanceClassSpecificViewport.js', 'client');
|
||||
api.addFiles('lib/setMammogramViewportAlignment.js', 'client');
|
||||
|
||||
//api.export('accountsConfig', 'client');
|
||||
api.export('resizeViewportElements', 'client');
|
||||
api.export('handleResize', 'client');
|
||||
api.export('enableHotkeys', 'client');
|
||||
@@ -162,7 +142,6 @@ Package.onUse(function(api) {
|
||||
api.export('getImageId', 'client');
|
||||
api.export('getWADORSImageId', 'client');
|
||||
api.export('metaDataProvider', 'client');
|
||||
api.export('rerenderViewportWithNewSeries', 'client');
|
||||
api.export('sortStudy', 'client');
|
||||
api.export('updateOrientationMarkers', 'client');
|
||||
api.export('setFocusToActiveViewport', 'client');
|
||||
@@ -172,55 +151,46 @@ Package.onUse(function(api) {
|
||||
api.export('exportStudies', 'client');
|
||||
api.export('importStudies', 'client');
|
||||
api.export('getActiveViewportElement', 'client');
|
||||
api.export('encodeQueryData', 'server');
|
||||
api.export('getInstanceClassDefaultViewport', 'client');
|
||||
api.export('showConfirmDialog', 'client');
|
||||
api.export('applyWLPreset', 'client');
|
||||
|
||||
// Export the ValidateJS Library with our added validators
|
||||
//api.export('validate', 'client');
|
||||
|
||||
// Viewer management objects
|
||||
api.export('toolManager', 'client');
|
||||
api.export('WindowManager', 'client');
|
||||
api.export('LayoutManager', 'client');
|
||||
|
||||
// Global objects
|
||||
api.export('OHIF', 'client');
|
||||
api.export('ClientId', 'client');
|
||||
api.export('progressDialog', 'client');
|
||||
|
||||
// Collections
|
||||
api.export('ViewerStudies', 'client');
|
||||
|
||||
// UI Helpers
|
||||
api.addFiles('lib/helpers/formatDA.js', 'client');
|
||||
api.addFiles('lib/helpers/formatJSDate.js', 'client');
|
||||
api.addFiles('lib/helpers/jsDateFromNow.js', 'client');
|
||||
api.addFiles('lib/helpers/formatNumberPrecision.js', 'client');
|
||||
api.addFiles('lib/helpers/formatTM.js', 'client');
|
||||
api.addFiles('lib/helpers/inlineIf.js', 'client');
|
||||
|
||||
api.addFiles('lib/helpers/inc.js', 'client');
|
||||
api.addFiles('lib/helpers/getUsername.js', 'client');
|
||||
api.addFiles('lib/helpers/capitalizeFirstLetter.js', 'client');
|
||||
api.addFiles('lib/helpers/objectToPairs.js', 'client');
|
||||
api.addFiles('lib/helpers/objectEach.js', 'client');
|
||||
api.addFiles('lib/helpers/ifTypeIs.js', 'client');
|
||||
api.addFiles('lib/helpers/prettyPrintStringify.js', 'client');
|
||||
api.addFiles('lib/helpers/formatPN.js', 'client');
|
||||
api.export('formatPN', 'client');
|
||||
|
||||
api.addFiles('lib/helpers/isTouchDevice.js', 'client');
|
||||
api.export('isTouchDevice', 'client');
|
||||
|
||||
// Server-side functions
|
||||
api.addFiles('server/seed.js', 'server');
|
||||
api.addFiles('server/lib/namespace.js', 'server');
|
||||
api.addFiles('server/methods/getStudyMetadata.js', 'server');
|
||||
api.addFiles('server/methods/worklistSearch.js', 'server');
|
||||
api.addFiles('server/methods/importStudies.js', 'server');
|
||||
|
||||
// DICOMWeb instance, study, and metadata retrieval
|
||||
api.addFiles('server/services/qido/instances.js', 'server');
|
||||
api.addFiles('server/services/qido/studies.js', 'server');
|
||||
api.addFiles('server/services/wado/retrieveMetadata.js', 'server');
|
||||
|
||||
// DIMSE instance, study, and metadata retrieval
|
||||
api.addFiles('server/services/dimse/instances.js', 'server');
|
||||
api.addFiles('server/services/dimse/studies.js', 'server');
|
||||
api.addFiles('server/services/dimse/retrieveMetadata.js', 'server');
|
||||
|
||||
// Study, instance, and metadata retrieval from remote PACS via Orthanc as a proxy
|
||||
api.addFiles('server/services/remote/instances.js', 'server');
|
||||
api.addFiles('server/services/remote/studies.js', 'server');
|
||||
api.addFiles('server/services/remote/retrieveMetadata.js', 'server');
|
||||
|
||||
api.export('Services', 'server');
|
||||
api.export('importStudies', 'server');
|
||||
api.export('importSupported', 'server');
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
Services = {};
|
||||
Services.QIDO = {};
|
||||
Services.WADO = {};
|
||||
Services.DIMSE = {};
|
||||
Services.REMOTE = {};
|
||||
|
||||
remoteGetValue = function(obj) {
|
||||
return obj ? obj.Value : null;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
Meteor.methods({
|
||||
/**
|
||||
* Retrieves Study metadata given a Study Instance UID
|
||||
* This Meteor method is available from both the client and the server
|
||||
*/
|
||||
GetStudyMetadata: function(studyInstanceUid) {
|
||||
log.info('GetStudyMetadata(%s)', studyInstanceUid);
|
||||
|
||||
if (!Meteor.settings.dicomWeb) {
|
||||
throw 'No properly configured server was available over DICOMWeb';
|
||||
}
|
||||
|
||||
if (Meteor.settings.dicomWeb && Meteor.settings.defaultServiceType === 'dicomWeb') {
|
||||
// Get the server data. This is user-defined in the
|
||||
// config.json files used to run the Meteor server
|
||||
var server = Meteor.settings.dicomWeb.endpoints[0];
|
||||
return Services.WADO.RetrieveMetadata(server, studyInstanceUid);
|
||||
} else if (Meteor.settings.dimse && Meteor.settings.defaultServiceType === 'dimse') {
|
||||
return Services.DIMSE.RetrieveMetadata(studyInstanceUid);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
Meteor.methods({
|
||||
/**
|
||||
* Use the specified filter to conduct a search from the DICOM server
|
||||
* @param filter
|
||||
*/
|
||||
WorklistSearch: function(filter) {
|
||||
if (Meteor.settings.dicomWeb && Meteor.settings.defaultServiceType === 'dicomWeb') {
|
||||
// Get the server data. This is user-defined in the
|
||||
// config.json files used to run the Meteor server
|
||||
var server = Meteor.settings.dicomWeb.endpoints[0];
|
||||
|
||||
return Services.QIDO.Studies(server, filter);
|
||||
} else if (Meteor.settings.dimse && Meteor.settings.defaultServiceType === 'dimse') {
|
||||
return Services.DIMSE.Studies(filter);
|
||||
} else {
|
||||
throw 'No properly configured server was available over DICOMWeb or DIMSE.';
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
var users = [
|
||||
{
|
||||
email: 'admin@ohif.org',
|
||||
//username: "admin",
|
||||
password: "admin",
|
||||
profile: {
|
||||
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
Meteor.startup(function() {
|
||||
users.forEach(function(user) {
|
||||
if(!Meteor.users.findOne({username: user.username})) {
|
||||
Accounts.createUser(user);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
/**
|
||||
* Parses data returned from a study search and transforms it into
|
||||
* an array of series that are present in the study
|
||||
*
|
||||
* @param resultData
|
||||
* @returns {Array} Series List
|
||||
*/
|
||||
function resultDataToStudyMetadata(resultData) {
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
|
||||
resultData.forEach(function(instanceRaw) {
|
||||
var instance = instanceRaw.toObject();
|
||||
// Use seriesMap to cache series data
|
||||
// If the series instance UID has already been used to
|
||||
// process series data, continue using that series
|
||||
var seriesInstanceUid = instance[0x0020000E];
|
||||
var series = seriesMap[seriesInstanceUid];
|
||||
|
||||
// If no series data exists in the seriesMap cache variable,
|
||||
// process any available series data
|
||||
if (!series) {
|
||||
series = {
|
||||
seriesInstanceUid: seriesInstanceUid,
|
||||
seriesNumber: instance[0x00200011],
|
||||
instances: []
|
||||
};
|
||||
|
||||
// Save this data in the seriesMap cache variable
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
var host = Meteor.settings.dimse.host,
|
||||
port = Meteor.settings.dimse.port;
|
||||
|
||||
var serverRoot = host + ':' + port;
|
||||
|
||||
var sopInstanceUid = instance[0x00080018];
|
||||
var uri = serverRoot + '/studies/' + studyInstanceUid + '/series/' + seriesInstanceUid + '/instances/' + sopInstanceUid + '/frames/1';
|
||||
|
||||
// Add this instance to the current series
|
||||
series.instances.push({
|
||||
sopClassUid: instance[0x00080016],
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
uri: uri,
|
||||
instanceNumber: instance[0x00200013]
|
||||
});
|
||||
});
|
||||
return seriesList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a set of instances using a DIMSE call
|
||||
* @param studyInstanceUid
|
||||
* @returns {{wadoUriRoot: String, studyInstanceUid: String, seriesList: Array}}
|
||||
*/
|
||||
Services.DIMSE.Instances = function(studyInstanceUid) {
|
||||
//var url = buildUrl(server, studyInstanceUid);
|
||||
var result = DIMSE.retrieveInstances(studyInstanceUid);
|
||||
|
||||
return {
|
||||
studyInstanceUid: studyInstanceUid,
|
||||
seriesList: resultDataToStudyMetadata(result)
|
||||
};
|
||||
};
|
||||
@@ -1,126 +0,0 @@
|
||||
/**
|
||||
* Parses the SourceImageSequence, if it exists, in order
|
||||
* to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID
|
||||
* is used to refer to this image in any accompanying DICOM-SR documents.
|
||||
*
|
||||
* @param instance
|
||||
* @returns {String} The ReferenceSOPInstanceUID
|
||||
*/
|
||||
function getSourceImageInstanceUid(instance) {
|
||||
// TODO= Parse the whole Source Image Sequence
|
||||
// This is a really poor workaround for now.
|
||||
// Later we should probably parse the whole sequence.
|
||||
var SourceImageSequence = instance[0x00082112];
|
||||
log.info('SourceImageSequence ' + SourceImageSequence);
|
||||
if (SourceImageSequence && SourceImageSequence.length) {
|
||||
return SourceImageSequence[0][0x00081155];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses result data from a DIMSE search into Study MetaData
|
||||
* Returns an object populated with study metadata, including the
|
||||
* series list.
|
||||
*
|
||||
* @param resultData
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
function resultDataToStudyMetadata(studyInstanceUid, resultData) {
|
||||
log.info('resultDataToStudyMetadata');
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
|
||||
if (!resultData.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var anInstance = resultData[0].toObject();
|
||||
if (!anInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
var studyData = {
|
||||
seriesList: seriesList,
|
||||
patientName: anInstance[0x00100010],
|
||||
patientId: anInstance[0x00100020],
|
||||
accessionNumber: anInstance[0x00080050],
|
||||
studyDate: anInstance[0x00080020],
|
||||
modalities: anInstance[0x00080061],
|
||||
studyDescription: anInstance[0x00081030],
|
||||
imageCount: anInstance[0x00201208],
|
||||
studyInstanceUid: anInstance[0x0020000D]
|
||||
};
|
||||
|
||||
resultData.forEach(function(instanceRaw) {
|
||||
var instance = instanceRaw.toObject();
|
||||
var seriesInstanceUid = instance[0x0020000E];
|
||||
var series = seriesMap[seriesInstanceUid];
|
||||
if (!series) {
|
||||
series = {
|
||||
seriesDescription: instance[0x0008103E],
|
||||
modality: instance[0x00080060],
|
||||
seriesInstanceUid: seriesInstanceUid,
|
||||
seriesNumber: parseFloat(instance[0x00200011]),
|
||||
instances: []
|
||||
};
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
var sopInstanceUid = instance[0x00080018];
|
||||
|
||||
var instanceSummary = {
|
||||
imageType: instance[0x00080008],
|
||||
sopClassUid: instance[0x00080016],
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
instanceNumber: parseFloat(instance[0x00200013]),
|
||||
imagePositionPatient: instance[0x00200032],
|
||||
imageOrientationPatient: instance[0x00200037],
|
||||
frameOfReferenceUID: instance[0x00200052],
|
||||
sliceLocation: parseFloat(instance[0x00201041]),
|
||||
samplesPerPixel: parseFloat(instance[0x00280002]),
|
||||
photometricInterpretation: instance[0x00280004],
|
||||
rows: parseFloat(instance[0x00280010]),
|
||||
columns: parseFloat(instance[0x00280011]),
|
||||
pixelSpacing: instance[0x00280030],
|
||||
bitsAllocated: parseFloat(instance[0x00280100]),
|
||||
bitsStored: parseFloat(instance[0x00280101]),
|
||||
highBit: parseFloat(instance[0x00280102]),
|
||||
pixelRepresentation: parseFloat(instance[0x00280103]),
|
||||
windowCenter: instance[0x00281050],
|
||||
windowWidth: instance[0x00281051],
|
||||
rescaleIntercept: parseFloat(instance[0x00281052]),
|
||||
rescaleSlope: parseFloat(instance[0x00281053]),
|
||||
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
|
||||
laterality: instance[0x00200062],
|
||||
viewPosition: instance[0x00185101],
|
||||
numFrames: parseFloat(instance[0x00280008]),
|
||||
frameTime: parseFloat(instance[0x00181063])
|
||||
};
|
||||
|
||||
// Retrieve the actual data over WADO-URI
|
||||
var server = Meteor.settings.dicomWeb.endpoints[0];
|
||||
instanceSummary.wadouri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom";
|
||||
|
||||
series.instances.push(instanceSummary);
|
||||
});
|
||||
|
||||
return studyData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieved Study MetaData from a DICOM server using DIMSE
|
||||
* @param studyInstanceUid
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
Services.DIMSE.RetrieveMetadata = function(studyInstanceUid) {
|
||||
var result = DIMSE.retrieveInstances(studyInstanceUid);
|
||||
|
||||
var study = resultDataToStudyMetadata(studyInstanceUid, result);
|
||||
if (!study) {
|
||||
study = {};
|
||||
}
|
||||
|
||||
study.studyInstanceUid = studyInstanceUid;
|
||||
return study;
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
/**
|
||||
* Parses resulting data from a QIDO call into a set of Study MetaData
|
||||
*
|
||||
* @param resultData
|
||||
* @returns {Array} An array of Study MetaData objects
|
||||
*/
|
||||
function resultDataToStudies(resultData) {
|
||||
var studies = [];
|
||||
|
||||
resultData.forEach(function(studyRaw) {
|
||||
var study = studyRaw.toObject();
|
||||
studies.push({
|
||||
studyInstanceUid: study[0x0020000D],
|
||||
// 00080005 = SpecificCharacterSet
|
||||
studyDate: study[0x00080020],
|
||||
studyTime: study[0x00080030],
|
||||
accessionNumber: study[0x00080050],
|
||||
referringPhysicianName: study[0x00080090],
|
||||
// 00081190 = URL
|
||||
patientName: study[0x00100010],
|
||||
patientId: study[0x00100020],
|
||||
patientBirthdate: study[0x00100030],
|
||||
patientSex: study[0x00100040],
|
||||
imageCount: study[0x00201208],
|
||||
studyId: study[0x00200010],
|
||||
studyDescription: study[0x00081030],
|
||||
modalities: study[0x00080061]
|
||||
});
|
||||
});
|
||||
return studies;
|
||||
}
|
||||
|
||||
Services.DIMSE.Studies = function(filter) {
|
||||
log.info('Services.DIMSE.Studies');
|
||||
var parameters = {
|
||||
0x00100010: filter.patientName,
|
||||
0x00100020: filter.patientId,
|
||||
0x00080050: filter.accessionNumber,
|
||||
0x00081030: filter.studyDescription,
|
||||
0x00100040: '',
|
||||
0x00201208: '',
|
||||
0x00080061: ''
|
||||
};
|
||||
|
||||
var results = DIMSE.retrieveStudies(parameters);
|
||||
return resultDataToStudies(results);
|
||||
};
|
||||
@@ -1,83 +0,0 @@
|
||||
/**
|
||||
* Creates a QIDO URL given the server settings and a study instance UID
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {string} URL to be used for QIDO calls
|
||||
*/
|
||||
function buildUrl(server, studyInstanceUid) {
|
||||
return server.qidoRoot + '/studies/' + studyInstanceUid + '/instances';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses data returned from a QIDO search and transforms it into
|
||||
* an array of series that are present in the study
|
||||
*
|
||||
* @param server The DICOM server
|
||||
* @param studyInstanceUid
|
||||
* @param resultData
|
||||
* @returns {Array} Series List
|
||||
*/
|
||||
function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
|
||||
resultData.forEach(function(instance) {
|
||||
// Use seriesMap to cache series data
|
||||
// If the series instance UID has already been used to
|
||||
// process series data, continue using that series
|
||||
var seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
|
||||
var series = seriesMap[seriesInstanceUid];
|
||||
|
||||
// If no series data exists in the seriesMap cache variable,
|
||||
// process any available series data
|
||||
if (!series) {
|
||||
series = {
|
||||
seriesInstanceUid: seriesInstanceUid,
|
||||
seriesNumber: DICOMWeb.getString(instance['00200011']),
|
||||
instances: []
|
||||
};
|
||||
|
||||
// Save this data in the seriesMap cache variable
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
// The uri for the dicomweb
|
||||
// NOTE: DCM4CHEE seems to return the data zipped
|
||||
// NOTE: Orthanc returns the data with multi-part mime which cornerstoneWADOImageLoader doesn't
|
||||
// know how to parse yet
|
||||
//var uri = DICOMWeb.getString(instance['00081190']);
|
||||
//uri = uri.replace('wado-rs', 'dicom-web');
|
||||
|
||||
// manually create a WADO-URI from the UIDs
|
||||
// NOTE: Haven't been able to get Orthanc's WADO-URI to work yet - maybe its not configured?
|
||||
var sopInstanceUid = DICOMWeb.getString(instance['00080018']);
|
||||
var uri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + '&contentType=application%2Fdicom';
|
||||
|
||||
// Add this instance to the current series
|
||||
series.instances.push({
|
||||
sopClassUid: DICOMWeb.getString(instance['00080016']),
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
uri: uri,
|
||||
instanceNumber: DICOMWeb.getString(instance['00200013'])
|
||||
});
|
||||
});
|
||||
return seriesList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a set of instances using a QIDO call
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {{wadoUriRoot: String, studyInstanceUid: String, seriesList: Array}}
|
||||
*/
|
||||
Services.QIDO.Instances = function(server, studyInstanceUid) {
|
||||
var url = buildUrl(server, studyInstanceUid);
|
||||
var result = DICOMWeb.getJSON(url, server.requestOptions);
|
||||
|
||||
return {
|
||||
wadoUriRoot: server.wadoUriRoot,
|
||||
studyInstanceUid: studyInstanceUid,
|
||||
seriesList: resultDataToStudyMetadata(server, studyInstanceUid, result.data)
|
||||
};
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
/**
|
||||
* Produces a QIDO URL given server details and a set of specified search filter
|
||||
* items
|
||||
*
|
||||
* @param server
|
||||
* @param filter
|
||||
* @returns {string} The URL with encoded filter query data
|
||||
*/
|
||||
function filterToQIDOURL(server, filter) {
|
||||
var commaSeparatedFields = [
|
||||
'00081030', // Study Description
|
||||
'00080060' //Modality
|
||||
// Add more fields here if you want them in the Study List
|
||||
].join(',');
|
||||
|
||||
var parameters = {
|
||||
PatientName: filter.patientName,
|
||||
PatientID: filter.patientId,
|
||||
AccessionNumber: filter.accessionNumber,
|
||||
StudyDescription: filter.studyDescription,
|
||||
limit: filter.limit || 20,
|
||||
includefield: server.qidoSupportsIncludeField ? 'all' : commaSeparatedFields
|
||||
};
|
||||
return server.qidoRoot + '/studies?' + encodeQueryData(parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses resulting data from a QIDO call into a set of Study MetaData
|
||||
*
|
||||
* @param resultData
|
||||
* @returns {Array} An array of Study MetaData objects
|
||||
*/
|
||||
function resultDataToStudies(resultData) {
|
||||
var studies = [];
|
||||
|
||||
resultData.forEach(function(study) {
|
||||
studies.push({
|
||||
studyInstanceUid: DICOMWeb.getString(study['0020000D']),
|
||||
// 00080005 = SpecificCharacterSet
|
||||
studyDate: DICOMWeb.getString(study['00080020']),
|
||||
studyTime: DICOMWeb.getString(study['00080030']),
|
||||
accessionNumber: DICOMWeb.getNumber(study['00080050']),
|
||||
referringPhysicianName: DICOMWeb.getString(study['00080090']),
|
||||
// 00081190 = URL
|
||||
patientName: DICOMWeb.getName(study['00100010']),
|
||||
patientId: DICOMWeb.getString(study['00100020']),
|
||||
patientBirthdate: DICOMWeb.getString(study['00100030']),
|
||||
patientSex: DICOMWeb.getString(study['00100040']),
|
||||
studyId: DICOMWeb.getString(study['00200010']),
|
||||
numberOfStudyRelatedSeries: DICOMWeb.getString(study['00201206']),
|
||||
numberOfStudyRelatedInstances: DICOMWeb.getString(study['00201208']),
|
||||
studyDescription: DICOMWeb.getString(study['00081030']),
|
||||
// modality: DICOMWeb.getString(study['00080060']),
|
||||
// modalitiesInStudy: DICOMWeb.getString(study['00080061']),
|
||||
modalities: DICOMWeb.getString(DICOMWeb.getModalities(study['00080060'], study['00080061']))
|
||||
});
|
||||
});
|
||||
return studies;
|
||||
}
|
||||
|
||||
Services.QIDO.Studies = function(server, filter) {
|
||||
var url = filterToQIDOURL(server, filter);
|
||||
var result = DICOMWeb.getJSON(url, server.requestOptions);
|
||||
return resultDataToStudies(result.data);
|
||||
};
|
||||
@@ -1,82 +0,0 @@
|
||||
/**
|
||||
* Parses data returned from a QIDO search and transforms it into
|
||||
* an array of series that are present in the study
|
||||
*
|
||||
* @param server The DICOM server
|
||||
* @param studyInstanceUid
|
||||
* @param resultData
|
||||
* @returns {Array} Series List
|
||||
*/
|
||||
function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
|
||||
resultData.forEach(function(instance) {
|
||||
// Use seriesMap to cache series data
|
||||
// If the series instance UID has already been used to
|
||||
// process series data, continue using that series
|
||||
var seriesInstanceUid = remoteGetValue(instance['0020,000e']);
|
||||
var series = seriesMap[seriesInstanceUid];
|
||||
|
||||
// If no series data exists in the seriesMap cache variable,
|
||||
// process any available series data
|
||||
if(!series) {
|
||||
series = {
|
||||
seriesInstanceUid : seriesInstanceUid,
|
||||
seriesNumber : parseFloat(remoteGetValue(instance['0020,0011'])),
|
||||
instances: []
|
||||
};
|
||||
|
||||
// Save this data in the seriesMap cache variable
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
// The uri for the dicomweb
|
||||
// NOTE: DCM4CHEE seems to return the data zipped
|
||||
// NOTE: Orthanc returns the data with multi-part mime which cornerstoneWADOImageLoader doesn't
|
||||
// know how to parse yet
|
||||
//var uri = DICOMWeb.getString(instance['00081190']);
|
||||
//uri = uri.replace('wado-rs', 'dicom-web');
|
||||
|
||||
// manually create a WADO-URI from the UIDs
|
||||
// NOTE: Haven't been able to get Orthanc's WADO-URI to work yet - maybe its not configured?
|
||||
var sopInstanceUid = remoteGetValue(instance['0008,0018']);
|
||||
var uri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom";
|
||||
|
||||
// Add this instance to the current series
|
||||
series.instances.push({
|
||||
sopClassUid: remoteGetValue(instance['0008,0016']),
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
uri: uri,
|
||||
instanceNumber: parseFloat(remoteGetValue(instance['0020,0013']))
|
||||
});
|
||||
});
|
||||
return seriesList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a set of instances using a QIDO call
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {{wadoUriRoot: String, studyInstanceUid: String, seriesList: Array}}
|
||||
*/
|
||||
Services.REMOTE.Instances = function(server, studyInstanceUid) {
|
||||
var parameters = {
|
||||
PatientName: "",
|
||||
PatientID: "",
|
||||
AccessionNumber: "",
|
||||
SeriesInstanceUID: "",
|
||||
SeriesNumber : "",
|
||||
SOPClassUID : "",
|
||||
InstanceNumber : ""
|
||||
};
|
||||
|
||||
var remote = new OrthancRemote(server.root, server.sourceAE);
|
||||
|
||||
return {
|
||||
wadoUriRoot: server.wadoUriRoot,
|
||||
studyInstanceUid: studyInstanceUid,
|
||||
seriesList: resultDataToStudyMetadata(server, studyInstanceUid, remote.findInstances(server.modality, studyInstanceUid, null, parameters))
|
||||
};
|
||||
};
|
||||
@@ -1,133 +0,0 @@
|
||||
/**
|
||||
* Parses the SourceImageSequence, if it exists, in order
|
||||
* to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID
|
||||
* is used to refer to this image in any accompanying DICOM-SR documents.
|
||||
*
|
||||
* @param instance
|
||||
* @returns {String} The ReferenceSOPInstanceUID
|
||||
*/
|
||||
function getSourceImageInstanceUid(instance) {
|
||||
// TODO= Parse the whole Source Image Sequence
|
||||
// This is a really poor workaround for now.
|
||||
// Later we should probably parse the whole sequence.
|
||||
var SourceImageSequence = remoteGetValue(instance['0008,2112']);
|
||||
if (SourceImageSequence && SourceImageSequence.Value && SourceImageSequence.Value.length) {
|
||||
return SourceImageSequence.Value[0]['0008,1155'].Value[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses result data from a WADO search into Study MetaData
|
||||
* Returns an object populated with study metadata, including the
|
||||
* series list.
|
||||
*
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @param resultData
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
|
||||
if (!resultData.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var anInstance = resultData[0];
|
||||
if (!anInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
var studyData = {
|
||||
seriesList: seriesList,
|
||||
patientName: remoteGetValue(anInstance['0010,0010']),
|
||||
patientId: remoteGetValue(anInstance['0010,0020']),
|
||||
accessionNumber: remoteGetValue(anInstance['0008,0050']),
|
||||
studyDate: remoteGetValue(anInstance['0008,0020']),
|
||||
modalities: remoteGetValue(anInstance['0008,0061']),
|
||||
studyDescription: remoteGetValue(anInstance['0008,1030']),
|
||||
imageCount: remoteGetValue(anInstance['0020,1208']),
|
||||
studyInstanceUid: remoteGetValue(anInstance['0020,000d'])
|
||||
};
|
||||
|
||||
resultData.forEach(function(instance) {
|
||||
var seriesInstanceUid = remoteGetValue(instance['0020,000e']);
|
||||
var series = seriesMap[seriesInstanceUid];
|
||||
if (!series) {
|
||||
series = {
|
||||
seriesDescription: remoteGetValue(instance['0008,103e']),
|
||||
modality: remoteGetValue(instance['0008,0060']),
|
||||
seriesInstanceUid: seriesInstanceUid,
|
||||
seriesNumber: parseFloat(remoteGetValue(instance['0020,0011'])),
|
||||
instances: []
|
||||
};
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
var sopInstanceUid = remoteGetValue(instance['0008,0018']);
|
||||
|
||||
var instanceSummary = {
|
||||
imageType: remoteGetValue(instance['0008,0008']),
|
||||
sopClassUid: remoteGetValue(instance['0008,0016']),
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
instanceNumber: parseFloat(remoteGetValue(instance['0020,0013'])),
|
||||
imagePositionPatient: remoteGetValue(instance['0020,0032']),
|
||||
imageOrientationPatient: remoteGetValue(instance['0020,0037']),
|
||||
frameOfReferenceUID: remoteGetValue(instance['0020,0052']),
|
||||
sliceLocation: parseFloat(remoteGetValue(instance['0020,1041'])),
|
||||
samplesPerPixel: parseFloat(remoteGetValue(instance['0028,0002'])),
|
||||
photometricInterpretation: remoteGetValue(instance['0028,0004']),
|
||||
rows: parseFloat(remoteGetValue(instance['0028,0010'])),
|
||||
columns: parseFloat(remoteGetValue(instance['0028,0011'])),
|
||||
pixelSpacing: remoteGetValue(instance['0028,0030']),
|
||||
bitsAllocated: parseFloat(remoteGetValue(instance['0028,0100'])),
|
||||
bitsStored: parseFloat(remoteGetValue(instance['0028,0101'])),
|
||||
highBit: parseFloat(remoteGetValue(instance['0028,0102'])),
|
||||
pixelRepresentation: parseFloat(remoteGetValue(instance['0028,0103'])),
|
||||
windowCenter: remoteGetValue(instance['0028,1050']),
|
||||
windowWidth: remoteGetValue(instance['0028,1051']),
|
||||
rescaleIntercept: parseFloat(remoteGetValue(instance['0028,1052'])),
|
||||
rescaleSlope: parseFloat(remoteGetValue(instance['0028,1053'])),
|
||||
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
|
||||
laterality: remoteGetValue(instance['0020,0062']),
|
||||
viewPosition: remoteGetValue(instance['0018,5101']),
|
||||
numFrames: parseFloat(remoteGetValue(instance['0028,0008'])),
|
||||
frameTime: parseFloat(remoteGetValue(instance['0018,1063']))
|
||||
};
|
||||
|
||||
var iid = instance['xxxx,0001'].Value;
|
||||
if (server.imageRendering === 'wadouri') {
|
||||
instanceSummary.wadouri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom";
|
||||
} else if (server.imageRendering == 'orthanc') {
|
||||
instanceSummary.wadouri = server.root + '/instances/' + iid + '/file';
|
||||
} else {
|
||||
instanceSummary.wadorsuri = server.wadoRoot + '/studies/' + studyInstanceUid + '/series/' + seriesInstanceUid + '/instances/' + sopInstanceUid + '/frames/1';
|
||||
}
|
||||
|
||||
series.instances.push(instanceSummary);
|
||||
});
|
||||
console.log(studyData.seriesList[0].instances);
|
||||
return studyData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieved Study MetaData from a DICOM server using a WADO call
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
Services.REMOTE.RetrieveMetadata = function(server, studyInstanceUid) {
|
||||
var remote = new OrthancRemote(server.root, server.sourceAE);
|
||||
|
||||
var study = resultDataToStudyMetadata(server, studyInstanceUid, remote.retrieveMetadata(server.modality, studyInstanceUid));
|
||||
if (!study) {
|
||||
study = {};
|
||||
}
|
||||
|
||||
study.wadoUriRoot = server.wadoUriRoot;
|
||||
study.studyInstanceUid = studyInstanceUid;
|
||||
|
||||
return study;
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
function resultDataToStudies(resultData) {
|
||||
var studies = [];
|
||||
|
||||
resultData.forEach(function(study) {
|
||||
studies.push({
|
||||
studyInstanceUid: remoteGetValue(study['0020,000d']),
|
||||
// 00080005 = SpecificCharacterSet
|
||||
studyDate: remoteGetValue(study['0008,0020']),
|
||||
studyTime: remoteGetValue(study['0008,0030']),
|
||||
accessionNumber: remoteGetValue(study['0008,0050']),
|
||||
referringPhysicianName: remoteGetValue(study['0008,0090']),
|
||||
// 00081190 = URL
|
||||
patientName: remoteGetValue(study['0010,0010']),
|
||||
patientId: remoteGetValue(study['0010,0020']),
|
||||
patientBirthdate: remoteGetValue(study['0010,0030']),
|
||||
patientSex: remoteGetValue(study['0010,0040']),
|
||||
studyId: remoteGetValue(study['0020,0010']),
|
||||
numberOfStudyRelatedSeries: parseFloat(remoteGetValue(study['0020,1206'])),
|
||||
numberOfStudyRelatedInstances: parseFloat(remoteGetValue(study['0020,1208'])),
|
||||
studyDescription: remoteGetValue(study['0008,1030']),
|
||||
modalities: remoteGetValue(study['0008,0061'])
|
||||
});
|
||||
});
|
||||
return studies;
|
||||
}
|
||||
|
||||
Services.REMOTE.Studies = function(server, filter) {
|
||||
var parameters = {
|
||||
PatientName: filter.patientName ? filter.patientName : "",
|
||||
PatientID: filter.patientId,
|
||||
AccessionNumber: filter.accessionNumber ? filter.accessionNumber : "",
|
||||
StudyDescription: "",
|
||||
StudyDate : "",
|
||||
StudyTime : "",
|
||||
ReferringPhysicianName : "",
|
||||
PatientBirthDate : "",
|
||||
PatientSex : "",
|
||||
StudyID : "",
|
||||
NumberOfStudyRelatedSeries : "",
|
||||
NumberOfStudyRelatedInstances : "",
|
||||
ModalitiesInStudy : ""
|
||||
};
|
||||
var remote = new OrthancRemote(server.root, server.sourceAE);
|
||||
var data = remote.findStudies(server.modality, parameters);
|
||||
|
||||
return resultDataToStudies(data.results);
|
||||
};
|
||||
@@ -1,144 +0,0 @@
|
||||
/**
|
||||
* Creates a URL for a WADO search
|
||||
*
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {string}
|
||||
*/
|
||||
function buildUrl(server, studyInstanceUid) {
|
||||
return server.wadoRoot + '/studies/' + studyInstanceUid + '/metadata';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the SourceImageSequence, if it exists, in order
|
||||
* to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID
|
||||
* is used to refer to this image in any accompanying DICOM-SR documents.
|
||||
*
|
||||
* @param instance
|
||||
* @returns {String} The ReferenceSOPInstanceUID
|
||||
*/
|
||||
function getSourceImageInstanceUid(instance) {
|
||||
// TODO= Parse the whole Source Image Sequence
|
||||
// This is a really poor workaround for now.
|
||||
// Later we should probably parse the whole sequence.
|
||||
var SourceImageSequence = instance['00082112'];
|
||||
if (SourceImageSequence && SourceImageSequence.Value && SourceImageSequence.Value.length) {
|
||||
return SourceImageSequence.Value[0]['00081155'].Value[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses result data from a WADO search into Study MetaData
|
||||
* Returns an object populated with study metadata, including the
|
||||
* series list.
|
||||
*
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @param resultData
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
|
||||
if (!resultData.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var anInstance = resultData[0];
|
||||
if (!anInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
var studyData = {
|
||||
seriesList: seriesList,
|
||||
patientName: DICOMWeb.getName(anInstance['00100010']),
|
||||
patientId: DICOMWeb.getString(anInstance['00100020']),
|
||||
accessionNumber: DICOMWeb.getString(anInstance['00080050']),
|
||||
studyDate: DICOMWeb.getString(anInstance['00080020']),
|
||||
modalities: DICOMWeb.getString(anInstance['00080061']),
|
||||
studyDescription: DICOMWeb.getString(anInstance['00081030']),
|
||||
imageCount: DICOMWeb.getString(anInstance['00201208']),
|
||||
studyInstanceUid: DICOMWeb.getString(anInstance['0020000D']),
|
||||
};
|
||||
|
||||
resultData.forEach(function(instance) {
|
||||
var seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
|
||||
var series = seriesMap[seriesInstanceUid];
|
||||
if (!series) {
|
||||
series = {
|
||||
seriesDescription: DICOMWeb.getString(instance['0008103E']),
|
||||
modality: DICOMWeb.getString(instance['00080060']),
|
||||
seriesInstanceUid: seriesInstanceUid,
|
||||
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
|
||||
instances: []
|
||||
};
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
var sopInstanceUid = DICOMWeb.getString(instance['00080018']);
|
||||
|
||||
var instanceSummary = {
|
||||
imageType: DICOMWeb.getString(instance['00080008']),
|
||||
sopClassUid: DICOMWeb.getString(instance['00080016']),
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
|
||||
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
|
||||
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
|
||||
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
|
||||
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
|
||||
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
|
||||
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
|
||||
rows: DICOMWeb.getNumber(instance['00280010']),
|
||||
columns: DICOMWeb.getNumber(instance['00280011']),
|
||||
pixelSpacing: DICOMWeb.getString(instance['00280030']),
|
||||
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
|
||||
bitsStored: DICOMWeb.getNumber(instance['00280101']),
|
||||
highBit: DICOMWeb.getNumber(instance['00280102']),
|
||||
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
|
||||
windowCenter: DICOMWeb.getString(instance['00281050']),
|
||||
windowWidth: DICOMWeb.getString(instance['00281051']),
|
||||
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
|
||||
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
|
||||
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
|
||||
laterality: DICOMWeb.getString(instance['00200062']),
|
||||
viewPosition: DICOMWeb.getString(instance['00185101']),
|
||||
numFrames: DICOMWeb.getNumber(instance['00280008']),
|
||||
frameTime: DICOMWeb.getNumber(instance['00181063']),
|
||||
sliceThickness: DICOMWeb.getNumber(instance['00180050'])
|
||||
};
|
||||
|
||||
if (server.imageRendering === 'wadouri') {
|
||||
instanceSummary.wadouri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom";
|
||||
} else {
|
||||
instanceSummary.wadorsuri = server.wadoRoot + '/studies/' + studyInstanceUid + '/series/' + seriesInstanceUid + '/instances/' + sopInstanceUid + '/frames/1';
|
||||
}
|
||||
|
||||
series.instances.push(instanceSummary);
|
||||
});
|
||||
|
||||
return studyData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieved Study MetaData from a DICOM server using a WADO call
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
Services.WADO.RetrieveMetadata = function(server, studyInstanceUid) {
|
||||
var url = buildUrl(server, studyInstanceUid);
|
||||
|
||||
var result = DICOMWeb.getJSON(url, server.requestOptions);
|
||||
|
||||
var study = resultDataToStudyMetadata(server, studyInstanceUid, result.data);
|
||||
if (!study) {
|
||||
study = {};
|
||||
}
|
||||
|
||||
study.wadoUriRoot = server.wadoUriRoot;
|
||||
study.studyInstanceUid = studyInstanceUid;
|
||||
|
||||
return study;
|
||||
};
|
||||
Reference in new issue
Block a user