Work on refactoring measurements out of Lesion Tracker package

- Removed AssociatedStudies Collection
- Measurement API is generated from configuration files
- Data exchange methods are defined in configuration
This commit is contained in:
Erik Ziegler committed 2016-11-15 08:21:46 +01:00
1 parent c511edd419
commit ab4d4c9008
537 files changed
+5122 -3545

No files matched your search

@@ -0,0 +1,99 @@
<template name="aboutModal">
<div class="modal fade"
id="aboutModal"
tabindex="-1"
role="dialog"
aria-labelledby="aboutModalLabel">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button"
class="close"
data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title"
id="aboutModalLabel">
About
</h4>
</div>
<div class="modal-body">
<a target='_blank'
class='forkOnGithub'
href="{{githubUrl}}">
<img src="https://camo.githubusercontent.com/365986a132ccd6a44c23a9169022c0b5c890c387/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f7265645f6161303030302e706e67"
alt="Fork me on GitHub"
data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_red_aa0000.png">
</a>
<div class="logo p-x-3 p-y-3">
<svg class="logoImage">
<use xlink:href="/packages/ohif_viewerbase/assets/icons.svg#icon-ohif-logo"></use>
</svg>
<a class="logoText"
target="_blank"
href="http://ohif.org">
Open Health Imaging Foundation DICOM Viewer
</a>
</div>
<div class="p-x-1 p-y-1 m-y-2">
<a class="btn btn-default inline"
target='_blank'
href="https://groups.google.com/forum/#!forum/cornerstone-platform">
Visit the forum
</a>
<a class="btn btn-default inline"
target='_blank'
href="https://github.com/OHIF/Viewers/issues">
Report an issue
</a>
</div>
<div class="p-y-1 p-x-1">
<h3>Version Information</h3>
<table class="table table-responsive">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Repository URL</td>
<td>
<a target='_blank'
href="{{githubUrl}}">{{githubUrl}}</a>
</td>
</tr>
<tr>
<td>Last Commit Hash</td>
<td>
<a target='_blank' href="{{githubUrl}}/commit/{{gitRev 'long'}}">
{{gitRev 'long'}}
</a>
</td>
</tr>
<tr>
<td>Latest Tag</td>
<td>
<a target='_blank' href="{{githubUrl}}/releases/tag/{{gitRev 'tag'}}">
{{gitRev 'tag'}}
</a>
</td>
</tr>
<tr>
<td>Branch</td>
<td>
<a target='_blank' href="{{githubUrl}}/tree/{{gitRev 'branch'}}">
{{gitRev 'branch'}}
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</template>
@@ -0,0 +1,5 @@
Template.aboutModal.helpers({
githubUrl() {
return 'https://github.com/OHIF/Viewers';
}
});
@@ -0,0 +1,24 @@
@import "{design}/app"
#aboutModal
.logo
.logoText
display: inline-block
font-family: $logoFontFamily
font-weight: $logoFontWeight
theme('color', '$textPrimaryColor')
font-size: 20pt
line-height: 45px
text-decoration: none
.logoImage
width: 45px
height: 45px
margin: 0 1rem
.forkOnGithub
position: absolute
top: 0
right: 0
border: 0
cursor: pointer
@@ -0,0 +1,3 @@
<template name="layout">
{{>yield }}
</template>
@@ -0,0 +1,5 @@
<template name="loadingText">
<div class="loadingTextDiv">
Loading... <i class="fa fa-spin fa-circle-o-notch fa-fw"></i>
</div>
</template>
@@ -0,0 +1,4 @@
.loadingTextDiv
margin: 30px
text-align: center
font-weight: 300
@@ -0,0 +1,7 @@
<template name="removableBackdrop">
<div class="removableBackdrop noselect"
oncontextmenu='return false;'
unselectable='on'
onselectstart='return false;'>
</div>
</template>
@@ -0,0 +1,9 @@
.removableBackdrop
position: fixed
top: 0
right: 0
bottom: 0
left: 0
z-index: 50
background-color: transparent
@@ -0,0 +1,11 @@
<template name="imageThumbnail">
<div class="imageThumbnail">
<div class="imageThumbnailCanvas"></div>
<div class="imageThumbnailLoadingIndicator thumbnailLoadingIndicator">
<p>Loading {{percentComplete}}</p>
</div>
<div class="imageThumbnailErrorLoadingIndicator thumbnailLoadingIndicator">
<p>Error</p>
</div>
</div>
</template>
@@ -0,0 +1,87 @@
Template.imageThumbnail.onRendered(() => {
const instance = Template.instance();
// Declare DOM and jQuery objects
const $parent = instance.$('.imageThumbnail');
const $loading = $parent.find('.imageThumbnailLoadingIndicator');
const $loadingError = $parent.find('.imageThumbnailErrorLoadingIndicator');
const $element = $parent.find('.imageThumbnailCanvas');
const element = $element.get(0);
instance.refreshImage = () => {
// Disable cornerstone for thumbnail element and remove its canvas
cornerstone.disable(element);
// Enable cornerstone for thumbnail element again creating a new canvas
cornerstone.enable(element);
// Get the image ID
const stack = instance.data.thumbnail.stack;
const thumbnailIndex = instance.data.thumbnail.thumbnailIndex;
const imageInstance = stack.images[0];
const imageId = getImageId(imageInstance);
// Activate the loading state
$loading.css('display', 'block');
// Add the current index on the global thumbnail loading controller
ThumbnailLoading[thumbnailIndex] = imageId;
// Define a handler for success on image load
const loadSuccess = image => {
cornerstone.displayImage(element, image);
delete ThumbnailLoading[thumbnailIndex];
$loading.css('display', 'none');
};
// Define a handler for error on image load
const loadError = () => {
$loading.css('display', 'none');
$loadingError.css('display', 'block');
};
// Call cornerstone image loader with the defined handlers
cornerstone.loadAndCacheImage(imageId).then(loadSuccess, loadError);
};
// Run this computation every time the current study is changed
instance.autorun(() => {
// Check if there is a reactive var set for current study
if (instance.data.currentStudy) {
// Register a dependency from this computation on current study
instance.data.currentStudy.dep.depend();
}
// Wait for the new data and reresh the image thumbnail
Tracker.afterFlush(() => {
instance.refreshImage();
});
});
});
Template.imageThumbnail.onDestroyed(() => {
const instance = Template.instance();
// Declare DOM and jQuery objects
const $parent = instance.$('.imageThumbnail');
const $element = $parent.find('.imageThumbnailCanvas');
const element = $element.get(0);
cornerstone.disable(element);
});
Template.imageThumbnail.helpers({
// Executed every time the image loading progress is changed
percentComplete() {
const instance = Template.instance();
const thumbnailIndex = instance.data.thumbnail.thumbnailIndex;
// Register a dependency from this computation on Session key
const percentComplete = Session.get('CornerstoneThumbnailLoadProgress' + thumbnailIndex);
// Return the complete percent amount of the image loading
if (percentComplete && percentComplete !== 100) {
return percentComplete + '%';
}
}
});
@@ -0,0 +1,49 @@
@import "{design}/app"
.thumbnailEntry.active .imageThumbnail
theme('border-color', '$activeColor')
box-shadow: none
transition($sidebarTransition)
.imageThumbnail
theme('box-shadow', 'inset 0 0 0 1px $uiBorderColorDark')
theme('background-color', '$primaryBackgroundColor')
border: 5px solid transparent
border-radius: 12px
height: 135px
margin: 0 auto
padding 1px 7px
position: relative
transition($sidebarTransition)
width: 217px
-moz-background-clip: padding
-webkit-background-clip: padding
background-clip: padding-box
.imageThumbnailClone
margin: 0 !important
&:hover
theme('border-color', '$hoverColor')
box-shadow: none
.imageThumbnailCanvas
height: 100%
overflow: hidden
.thumbnailLoadingIndicator
display: none
pointer-events: none
theme('color', '$textSecondaryColor')
height: 20px
width: 100%
top: 0
left: 0
right: 0
bottom: 0
margin: auto
position: absolute
p
text-align: center
font-size: 10pt
@@ -0,0 +1,11 @@
<template name="studyBrowser">
<div class="studyBrowser">
<div class="scrollableStudyThumbnails">
{{#each study in studies}}
{{#each thumbnail in (sort (studyThumbnails study) thumbnailSortBy thumbnailSortType)}}
{{>thumbnailEntry thumbnail=thumbnail}}
{{/each}}
{{/each}}
</div>
</div>
</template>
@@ -0,0 +1,7 @@
Template.studyBrowser.helpers({
studies() {
return ViewerStudies.find({
selected: true
});
}
});
@@ -0,0 +1,19 @@
.studyBrowser
float: left
height: 100%
width: 100%
overflow: hidden
background-color: black
padding-bottom: 20px
.scrollableStudyThumbnails
height: 100%
overflow-y: auto
overflow-x: hidden
padding-bottom: 50px
padding-right: 16px
padding-left: 4px
margin-right: -16px
&::-webkit-scrollbar
display: none
@@ -0,0 +1,30 @@
<template name="thumbnailEntry">
{{#let stack=this.thumbnail.stack}}
<div class="thumbnailEntry noselect m-t-1 m-b-2 {{draggableClass}} {{#if isDisplaySetActive stack.displaySetInstanceUid viewportIndex}}active{{/if}}">
<div class="p-x-1">
{{>imageThumbnail (clone this)}}
</div>
<div class="seriesDetails clearfix m-x-1 {{#unless stack.seriesDescription}}info-only{{/unless}}">
<div class="seriesDescription">
{{stack.seriesDescription}}
</div>
<div class="seriesInformation">
<div class="item item-series clearfix">
<div class="icon">S:</div>
<div class="value">{{stack.seriesNumber}}</div>
</div>
{{#unless isUndefined instanceNumber}}
<div class="item item-series clearfix">
<div class="icon">I:</div>
<div class="value">{{instanceNumber}}</div>
</div>
{{/unless}}
<div class="item item-frames clearfix">
<div class="icon"><div></div></div>
<div class="value">{{stack.numImageFrames}}</div>
</div>
</div>
</div>
</div>
{{/let}}
</template>
@@ -0,0 +1,279 @@
const cloneElement = (element, targetId) => {
// Clone the DOM element
const clone = element.cloneNode(true);
// Find any canvas children to clone
const clonedCanvases = $(clone).find('canvas');
clonedCanvases.each((canvasIndex, clonedCanvas) => {
// Draw from the original canvas to the cloned canvas
const context = clonedCanvas.getContext('2d');
const thumbnailCanvas = $(element).find('canvas').get(canvasIndex);
context.drawImage(thumbnailCanvas, 0, 0);
});
// Update the clone with the targetId
clone.id = targetId;
clone.style.visibility = 'hidden';
return clone;
};
const thumbnailDragStartHandler = (event, data) => {
// Prevent any scrolling behaviour normally caused by the original event
event.originalEvent.preventDefault();
// Identify the current study and series index from the thumbnail's DOM position
const targetThumbnail = event.currentTarget;
const $imageThumbnail = $(targetThumbnail);
// Clone the image thumbnail
const targetId = 'DragClone';
const clone = cloneElement(targetThumbnail, targetId);
const $clone = $(clone);
$clone.addClass('imageThumbnailClone');
// Set pointerEvents to pass through the clone DOM element
// This is necessary in order to identify what is below it
// when using document.elementFromPoint
clone.style.pointerEvents = 'none';
// Append the clone to the body
document.body.appendChild(clone);
// Set the cursor x and y positions from the current touch/mouse coordinates
let cursorX, cursorY;
// Handle touchStart cases
if (event.type === 'touchstart') {
cursorX = event.originalEvent.touches[0].pageX;
cursorY = event.originalEvent.touches[0].pageY;
} else {
cursorX = event.pageX;
cursorY = event.pageY;
// Also hook up event handlers for mouse events
const handlers = {};
handlers.mousemove = event => thumbnailDragHandler(event);
handlers.mouseup = event => thumbnailDragEndHandler(event, data, handlers);
$(document).on('mousemove', handlers.mousemove);
$(document).on('mouseup', handlers.mouseup);
}
// This block gets the current offset of the touch/mouse
// relative to the window
//
// i.e. Where did the user grab it from?
const offset = $imageThumbnail.offset();
const left = offset.left;
const top = offset.top;
// This difference is saved for later so the element movement looks normal
const diff = {
x: cursorX - left,
y: cursorY - top
};
$clone.data('diff', diff);
// This sets the default style properties of the cloned element so it is
// ready to be dragged around the page
$clone.css({
left: cursorX - diff.x,
position: 'fixed',
top: cursorY - diff.y,
visibility: 'hidden',
'z-index': 100000
});
};
const thumbnailDragHandler = event => {
// Get the touch/mouse coordinates from the event
let cursorX, cursorY;
if (event.type === 'touchmove') {
cursorX = event.originalEvent.changedTouches[0].pageX;
cursorY = event.originalEvent.changedTouches[0].pageY;
} else {
cursorX = event.pageX;
cursorY = event.pageY;
}
// Find the clone element and update it's position on the page
const $clone = $('#DragClone');
const diff = $clone.data('diff');
$clone.css({
left: cursorX - diff.x,
position: 'fixed',
top: cursorY - diff.y,
visibility: 'visible',
'z-index': 100000
});
// Identify the element below the current cursor position
const elemBelow = document.elementFromPoint(cursorX, cursorY);
// If none exists, stop here
if (!elemBelow) {
return;
}
// Remove any current faded effects on viewports
$('.imageViewerViewport canvas').removeClass('faded');
// Figure out what to do depending on what we're dragging over
const $viewportsDraggedOver = $(elemBelow).parents('.imageViewerViewport');
if ($viewportsDraggedOver.length) {
// If we're dragging over a non-empty viewport, fade it and change the cursor style
$viewportsDraggedOver.find('canvas').not('.magnifyTool').addClass('faded');
document.body.style.cursor = 'copy';
} else if (elemBelow.classList.contains('imageViewerViewport') && elemBelow.classList.contains('empty')) {
// If we're dragging over an empty viewport, just change the cursor style
document.body.style.cursor = 'copy';
} else {
// Otherwise, keep the cursor as no-drop style
document.body.style.cursor = 'no-drop';
}
};
const thumbnailDragEndHandler = (event, data, handlers) => {
// Remove the mouse event listeners
if (handlers) {
$(document).off('mousemove', handlers.mousemove);
$(document).off('mouseup', handlers.mouseup);
}
// Reset the cursor style to the default
document.body.style.cursor = 'auto';
// Get the cloned element
const $clone = $('#DragClone');
// If it doesn't exist, stop here
if (!$clone.length) {
return;
}
const top = $clone.offset().top;
const left = $clone.offset().left;
const diff = $clone.data('diff');
// Identify the element below the cloned element position
const elemBelow = document.elementFromPoint(left + diff.x, top + diff.y);
// Remove all cloned elements from the page
$('.imageThumbnailClone').remove();
// Remove any current faded effects on viewports
$('.imageViewerViewport canvas').removeClass('faded');
// If none exists, stop here
if (!elemBelow) {
return;
}
// Remove any fade effects on the element below
elemBelow.classList.remove('faded');
let element;
const $viewportsDraggedOver = $(elemBelow).closest('.imageViewerViewport');
if ($viewportsDraggedOver.length) {
// If we're dragging over a non-empty viewport, retrieve it
element = $viewportsDraggedOver.get(0);
} else if (elemBelow.classList.contains('imageViewerViewport') &&
elemBelow.classList.contains('empty')) {
// If we're dragging over an empty viewport, retrieve that instead
element = elemBelow;
} else {
// Otherwise, stop here
return false;
}
// If there is no stored drag and drop data, stop here
if (!data) {
return false;
}
// Get the dropped viewport index
const viewportIndex = $('.imageViewerViewport').index(element);
// Rerender the viewport using the dragged thumbnail data
window.layoutManager.rerenderViewportWithNewDisplaySet(viewportIndex, data);
return false;
};
Template.thumbnailEntry.onCreated(() => {
const instance = Template.instance();
// Check if the thumbnails will be draggable or clickable
instance.isDragAndDrop = _.isUndefined(instance.data.viewportIndex);
});
Template.thumbnailEntry.onRendered(() => {
console.warn('thumbnailEntry onRendered');
});
Template.thumbnailEntry.events({
// Event handlers for drag and drop
'touchstart .thumbnailEntry, mousedown .thumbnailEntry'(event, instance) {
const data = instance.data.thumbnail.stack;
instance.isDragAndDrop && thumbnailDragStartHandler(event, data);
},
'touchmove .thumbnailEntry'(event, instance) {
instance.isDragAndDrop && thumbnailDragHandler(event);
},
'touchend .thumbnailEntry'(event, instance) {
const data = instance.data.thumbnail.stack;
instance.isDragAndDrop && thumbnailDragEndHandler(event, data);
},
// Event handlers for click (quick switch)
'click .thumbnailEntry'(event, instance) {
if (!instance.isDragAndDrop) {
// Get the thumbnail stack data
const data = instance.data.thumbnail.stack;
// Rerender the viewport using the clicked thumbnail data
window.layoutManager.rerenderViewportWithNewDisplaySet(instance.data.viewportIndex, data);
}
},
// Event handlers for double click
'dblclick .thumbnailEntry'(event, instance) {
// Get the active viewport index
const viewportIndex = Session.get('activeViewport') || 0;
// Get the thumbnail stack data
const data = instance.data.thumbnail.stack;
// Rerender the viewport using the clicked thumbnail data
window.layoutManager.rerenderViewportWithNewDisplaySet(viewportIndex, data);
}
});
Template.thumbnailEntry.helpers({
draggableClass() {
return Template.instance().isDragAndDrop ? 'draggable' : '';
},
instanceNumber() {
const thumbnail = Template.instance().data.thumbnail;
if (!thumbnail) {
return;
}
const stack = thumbnail.stack;
if (!stack) {
return;
}
// No need to show instance number for single-frame images
if (!stack.isMultiFrame) {
return;
}
const firstImage = stack.images[0];
if (!firstImage) {
return;
}
return firstImage.instanceNumber;
}
});
@@ -0,0 +1,98 @@
@import "{design}/app"
$seriesCountBackgroundColor = #678696
.thumbnailEntry
cursor: pointer
display: table
margin: 0 auto
&.draggable
cursor: copy
cursor: -webkit-grab
cursor: -moz-grab
.seriesDetails
theme('color', '$textPrimaryColor')
font-size: 14px
line-height: 1.3em
margin-top: 5px
max-width: 217px
min-height: 36px
position: relative
word-wrap: break-word
&.info-only
min-height: auto
.seriesDescription
display: none
.seriesInformation
display: flex
float: none
max-width: none
padding-right: none
.item
flex: 1
text-align: center
.icon, .value
display: inline
float: none
line-height: 25px
.value
margin-left: 0
width: auto
.seriesDescription
float: left
width: calc(100% - 50px)
.seriesInformation
display: table
float: right
padding-right: 4px
max-width: 50px
.item-frames .icon
height: 18px
.value
theme('color', '$textSecondaryColor')
display: inline-block
float: right
font-size: 12px
margin-left: 4px
width: calc(100% - 15px)
.icon
theme('color', '$activeColor')
display: inline-block
float: left
font-size: 10px
font-weight: 900
text-align: right
width: 11px
div
background-color: $seriesCountBackgroundColor
margin-top: 6px
position: relative
&:after
theme('background-color', '$activeColor')
box-shadow: 1px 1px rgba(0, 0, 0, .115)
left: -5px
position: absolute
top: -5px
&
&:after
theme('border', '1px solid $primaryBackgroundColor')
content: ''
display: inline-block
height: 11px
width: 11px
@@ -0,0 +1,33 @@
<template name="annotationDialogs">
<dialog id="annotationDialog"
class="annotationDialog noselect">
<h5>Enter your annotation</h5>
<div class="annotationTextInputOptions">
<label for="annotationTextInput">New label</label>
<input name="annotationTextInput"
class="annotationTextInput"
type="text"
tabindex="-1"
autocomplete="off"/>
</div>
<a class="annotationDialogConfirm btn btn-sm btn-primary">OK</a>
</dialog>
<dialog id="relabelAnnotationDialog"
class="annotationDialog noselect"
oncontextmenu="return false">
<h5>Edit your annotation</h5>
<div class="annotationTextInputOptions">
<label for="annotationTextInput">New label</label>
<input name="annotationTextInput"
class="annotationTextInput"
type="text"
tabindex="-1"
autocomplete="off"/>
</div>
<div>
<a class="relabelRemove btn btn-sm btn-secondary">Remove marker</a>
<a class="relabelConfirm btn btn-sm btn-primary">OK</a>
</div>
</dialog>
</template>
@@ -0,0 +1,126 @@
// ------ Tool configuration functions ----- //
getAnnotationTextCallback = function(doneChangingTextCallback) {
// This handles the text entry for the annotation tool
function keyPressHandler(e) {
// If Enter or Esc are pressed, close the dialog
if (e.which === 13 || e.which === 27) {
closeHandler();
}
}
function closeHandler() {
dialog.get(0).close();
doneChangingTextCallback(getTextInput.val());
// Reset the text value
getTextInput.val('');
// Reset the focus to the active viewport element
// This makes the mobile Safari keyboard close
const element = getActiveViewportElement();
$(element).focus();
}
const dialog = $('#annotationDialog');
if (dialog.get(0).open === true) {
return;
}
const getTextInput = $('.annotationTextInput');
// Focus on the text input to open the Safari keyboard
getTextInput.focus();
dialog.get(0).showModal();
const confirm = dialog.find('.annotationDialogConfirm');
confirm.off('click');
confirm.on('click', () => {
closeHandler();
});
// Use keydown since keypress doesn't handle ESC in Chrome
dialog.off('keydown');
dialog.on('keydown', keyPressHandler);
};
changeAnnotationTextCallback = function(data, eventData, doneChangingTextCallback) {
const dialog = $('#relabelAnnotationDialog');
if (dialog.get(0).open === true) {
return;
}
if (isTouchDevice()) {
// Center the dialog on screen on touch devices
dialog.css({
top: 0,
left: 0,
right: 0,
bottom: 0,
margin: 'auto'
});
} else {
// Place the dialog above the tool that is being relabelled
dialog.css({
top: eventData.currentPoints.page.y - dialog.outerHeight() - 20,
left: eventData.currentPoints.page.x - dialog.outerWidth() / 2
});
}
const getTextInput = dialog.find('.annotationTextInput');
const confirm = dialog.find('.relabelConfirm');
const remove = dialog.find('.relabelRemove');
getTextInput.val(data.text);
// Focus on the text input to open the Safari keyboard
getTextInput.focus();
dialog.get(0).showModal();
confirm.off('click');
confirm.on('click', function() {
dialog.get(0).close();
doneChangingTextCallback(data, getTextInput.val());
});
// If the remove button is clicked, delete this marker
remove.off('click');
remove.on('click', function() {
dialog.get(0).close();
doneChangingTextCallback(data, undefined, true);
});
dialog.off('keydown');
dialog.on('keydown', keyPressHandler);
function keyPressHandler(e) {
// If Enter is pressed, close the dialog
if (e.which === 13) {
closeHandler();
}
}
function closeHandler() {
dialog.get(0).close();
doneChangingTextCallback(data, getTextInput.val());
// Reset the text value
getTextInput.val('');
// Reset the focus to the active viewport element
// This makes the mobile Safari keyboard close
const element = getActiveViewportElement();
$(element).focus();
}
};
Template.annotationDialogs.onRendered(() => {
const instance = Template.instance();
const dialogIds = ['annotationDialog', 'relabelAnnotationDialog'];
dialogIds.forEach(id => {
const dialog = instance.$('#' + id);
dialog.draggable();
dialogPolyfill.registerDialog(dialog.get(0));
});
});
@@ -0,0 +1,30 @@
@import "{design}/app.styl"
.annotationDialog
theme('border', '1px solid $uiBorderColor', 0.95)
theme('background', '$uiGrayDarkest', 0.95)
theme('color', '$textSecondaryColor')
z-index: 1000
position: absolute
top: 0
bottom: 0
left: 0
right: 0
margin: auto
overflow: hidden
padding: 10px
width: 300px
height: 140px
border-radius: 8px
h5, label
font-weight: 400
.annotationTextInputOptions
padding: 10px 0
.annotationTextInput
margin-left: 5px
.annotationDialogConfirm
float: right
@@ -0,0 +1,45 @@
<template name="cineDialog">
{{#form id='cineDialog' class=(getClassNames 'dialog-animated noselect') schema=instance.schema api=instance.api}}
{{>inputHidden key='intervalId'}}
<div class="cine-navigation">
<div class="btn-group">
{{#button disabled=(displaySetDisabled false) action='displaySetPrevious' title='Previous display set'}}
<i class="fa fa-toggle-up"></i>
{{/button}}
{{#button disabled=(displaySetDisabled true) action='displaySetNext' title='Next display set'}}
<i class="fa fa-toggle-down"></i>
{{/button}}
</div>
</div>
<div class="cine-controls">
<div class="btn-group">
{{#button action='cineFirst' title='Skip to first image' disabled=(buttonDisabled)}}
<i class="fa fa-fast-backward"></i>
{{/button}}
{{#button action='cinePrevious' title='Previous image' disabled=(buttonDisabled)}}
<i class="fa fa-step-backward"></i>
{{/button}}
{{#button action='cineToggle' title='Play / Stop' class=(valueIf isPlaying 'active' '') disabled=(buttonDisabled)}}
{{#if isPlaying}}
<i class="fa fa-fw fa-stop"></i>
{{else}}
<i class="fa fa-fw fa-play"></i>
{{/if}}
{{/button}}
{{#button action='cineNext' title='Next image' disabled=(buttonDisabled)}}
<i class="fa fa-step-forward"></i>
{{/button}}
{{#button action='cineLast' title='Skip to last image' disabled=(buttonDisabled)}}
<i class="fa fa-fast-forward"></i>
{{/button}}
</div>
</div>
<div class="cine-options">
<div class="fps-section">
{{#inputRange key='framesPerSecond' class='p-a-0' labelClass='form-group m-a-0' labelAsDiv=true}}
{{/inputRange}}
</div>
<span id="fps">{{framerate}} fps</span>
</div>
{{/form}}
</template>
@@ -0,0 +1,278 @@
import { Template } from 'meteor/templating';
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
import { OHIF } from 'meteor/ohif:core';
import { Session } from 'meteor/session';
import { _ } from 'meteor/underscore';
import { $ } from 'meteor/jquery';
Template.cineDialog.onCreated(() => {
const instance = Template.instance();
// Create the data schema for CINE controls
instance.schema = new SimpleSchema({
intervalId: {
type: Number,
optional: true
},
loop: {
type: Boolean,
label: 'Loop',
defaultValue: true
},
framesPerSecond: {
type: Number,
label: '',
defaultValue: 24,
min: 1,
max: 90,
optional: true
}
});
// Update the current viewport frame rate
instance.updateFramerate = rate => {
OHIF.viewer.cine.framesPerSecond = rate;
// Update playClip toolData for this imageId
const element = getActiveViewportElement();
if (!element) {
return;
}
const playClipToolData = cornerstoneTools.getToolState(element, 'playClip');
playClipToolData.data[0].framesPerSecond = OHIF.viewer.cine.framesPerSecond;
// If the movie is playing, stop/start to update the framerate
if (isPlaying()) {
cornerstoneTools.stopClip(element);
cornerstoneTools.playClip(element);
}
Session.set('UpdateCINE', Random.id());
};
// Define the actions API
instance.api = {
displaySetPrevious: () => OHIF.viewer.moveDisplaySets(false),
displaySetNext: () => OHIF.viewer.moveDisplaySets(true),
cineToggle: () => toggleCinePlay(),
cineFirst: () => switchToImageByIndex(0),
cineLast: () => switchToImageByIndex(-1),
cinePrevious: () => switchToImageRelative(-1),
cineNext: () => switchToImageRelative(1),
cineSlowDown: () => {
const newValue = OHIF.viewer.cine.framesPerSecond - 1;
if (newValue > 0) {
instance.updateFramerate(newValue);
}
},
cineSpeedUp: () => {
const newValue = OHIF.viewer.cine.framesPerSecond + 1;
if (newValue <= 90) {
instance.updateFramerate(newValue);
}
}
};
// Run this computation every time the active viewport is changed
instance.autorun(() => {
Session.get('activeViewport');
Tracker.afterFlush(() => {
// Get the active viewportElement
const element = getActiveViewportElement();
if (!element) {
return;
}
// Get the cornerstone playClip tool data
const toolData = cornerstoneTools.getToolState(element, 'playClip').data[0];
// Get the cine object
const cine = OHIF.viewer.cine;
// replace the cine values with the tool data
_.extend(cine, toolData);
// Set the defaults
cine.framesPerSecond = cine.framesPerSecond || 24;
cine.loop = _.isUndefined(cine.loop) ? true : cine.loop;
// Set the updated data on the form inputs
instance.$('form:first').data('component').value(cine);
// Update the session to refresh the framerate text
Session.set('UpdateCINE', Random.id());
});
});
/**
* Set/Reset Window resize handler. This function is a replacement for
* ... jQuery's on('resize', func) version which, for some unkown reason
* ... is currently not working for this portion of code.
* ... Further investigation is necessary.
*
* This happens because when an event is attached using jQuery's
* you can't get it using vanilla JavaScript, it returns null.
* You need to use jQuery for that. So, either you use vanilla JS or jQuery
* to get an element's event handler. See viewerMain for more details.
*/
instance.setResizeHandler = handler => {
if (typeof handler === 'function') {
const origHandler = window.onresize;
instance.origWindowResizeHandler = typeof origHandler === 'function' ? origHandler : null;
window.onresize = function (event) {
if (typeof origHandler === 'function') {
origHandler.call(window, event);
}
handler.call(window, event);
};
} else {
window.onresize = instance.origWindowResizeHandler || null;
window.origWindowResizeHandler = null;
}
};
/**
* Set optimal position for Cine dialog.
*/
instance.setOptimalPosition = (event, options) => {
const viewer = $('#viewer');
let toolbarElement = $('.toolbarSection .toolbarSectionTools:first'),
cineDialog = $('#cineDialog'),
cineDialogSize,
cineDialogCoords,
toolbarRect;
if (toolbarElement.length < 1 || cineDialog.length < 1) {
return;
}
if (cineDialog.data('wasDragged') || cineDialog.data('wasBounded')) {
// restore original handler...
instance.setResizeHandler(null);
return;
}
cineDialogSize = {
width: cineDialog.outerWidth() || 0,
height: cineDialog.outerHeight() || 0
};
toolbarRect = {
offset: toolbarElement.offset() || { top: 0, left: 0 },
width: toolbarElement.outerWidth() || 0,
height: toolbarElement.outerHeight() || 0
};
cineDialogCoords = {
left: toolbarRect.offset.left + toolbarRect.width + 20,
top: toolbarRect.offset.top + toolbarRect.height - cineDialogSize.height
};
if (options) {
if (options.left) {
cineDialogCoords.left = options.left;
}
if (options.top) {
cineDialogCoords.top = options.top;
}
}
// Check if it is out of screen
if (cineDialogCoords.top < 0) {
cineDialogCoords.top = 0;
}
else if (cineDialogCoords.top + cineDialogSize.height > viewer.height()) {
cineDialogCoords.top -= (cineDialogCoords.top + cineDialogSize.height) - viewer.height();
}
if (cineDialogCoords.left < 0) {
cineDialogCoords.left = 0;
}
else if (cineDialogCoords.left + cineDialogSize.width > viewer.width()) {
cineDialogCoords.left -= (cineDialogCoords.left + cineDialogSize.width) - viewer.width();
}
cineDialog.css(cineDialogCoords);
};
});
Template.cineDialog.onRendered(() => {
const instance = Template.instance();
let dialog = instance.$('#cineDialog'),
singleRowLayout = OHIF.uiSettings.displayEchoUltrasoundWorkflow;
// set dialog in optimal position and make sure it continues in a optimal position...
// ... when the window has been resized
instance.setOptimalPosition(null, {
top: singleRowLayout ? 47 : 26
});
// The jQuery method does not seem to be working...
// ... $(window).resize(instance.setOptimalPosition)
// This requires additional investigation.
instance.setResizeHandler(instance.setOptimalPosition);
// Make the CINE dialog bounded and draggable
dialog.bounded().draggable({ defaultElementCursor: 'move' });
// Prevent dialog from being dragged when user clicks any button
dialog.find('.cine-navigation, .cine-controls, .cine-options').on('mousedown touchstart', function (e) {
e.stopPropagation();
});
});
Template.cineDialog.onDestroyed(() => {
const instance = Template.instance();
// remove resize handler...
instance.setResizeHandler(null);
});
Template.cineDialog.events({
'change [data-key=loop] input'(event, instance) {
const element = getActiveViewportElement();
const playClipToolData = cornerstoneTools.getToolState(element, 'playClip');
playClipToolData.data[0].loop = $(event.currentTarget).is(':checked');
OHIF.viewer.cine.loop = playClipToolData.data[0].loop;
},
'input [data-key=framesPerSecond] input'(event, instance) {
// Update the FPS text onscreen
const rate = parseFloat($(event.currentTarget).val());
instance.updateFramerate(rate);
}
});
Template.cineDialog.helpers({
isPlaying() {
return isPlaying();
},
framerate() {
Session.get('UpdateCINE');
return OHIF.viewer.cine.framesPerSecond.toFixed(1);
},
displaySetDisabled(isNext) {
Session.get('LayoutManagerUpdated');
return !OHIF.viewer.canMoveDisplaySets(isNext) ? 'disabled' : '';
},
buttonDisabled() {
return hasMultipleFrames();
},
getClassNames(baseCls) {
return baseCls + ' ' + (OHIF.uiSettings.displayEchoUltrasoundWorkflow ? 'single' : 'double') + '-row-style'
}
});
@@ -0,0 +1,136 @@
@import "{design}/app.styl"
#cineDialog
theme('border', '2px solid $uiBorderColor', 0.95)
theme('background', '$uiGrayDarkest', 0.95)
theme('color', '$textSecondaryColor')
position: absolute
border-radius: 8px
z-index: 1000
overflow: hidden
cursor: move
h5
font-size: 20px
line-height: 35px
margin: 0
h5, label
font-weight: 400
.btn
theme('color', '$textSecondaryColor')
background-color: transparent
&:hover
theme('color', '$hoverColor')
&:active, &.active
theme('color', '$activeColor')
&[disabled]
&:hover
color: inherit
.cine-navigation, .cine-controls, .cine-options
cursor: default
#cineDialog.double-row-style
box-sizing: border-box
width: 300px
height: 80px
padding: 10px
top: 2%
left: 35%
.cine-navigation
position: absolute
right: 4px
top: 4px
.btn
padding: 0 4px
i
font-size: 36px
line-height: 36px
.cine-controls
left: 0px
.cine-options
padding: 0px 0
.fps-section
width: 175px
float: left
#fps
float: right
padding: 5px 10px 0 0
#cineDialog.single-row-style
box-sizing: content-box
width: 425px
height: 45px
padding: 4px 8px
top: 2%
left: 35%
.cine-navigation
position: relative
float: right
overflow: hidden
.btn
padding: 0 2px
margin: 0 0 0 2px
border: 0 none
&:first-of-type
margin-left: 0
i
font-size: 38px
line-height: 45px
.cine-controls
position: relative
float: left
overflow: hidden
.btn
font-size: 22px
line-height: 45px
min-width: 28px
padding: 0 0px
margin: 0 4px
border: 0 none
.cine-options
display: block
position: relative
width: 150px
float: left
overflow: hidden
padding: 0
margin: 0 0 0 10px
.fps-section
display: block
float: left
width: 80px
input[type="range"]
line-height: 45px
height: 45px
background-color: transparent
border: 0 none
outline: 0 none
#fps
display: block
width: 68px
float: left
padding: 0
margin: 0 0 0 2px
text-align: center
line-height: 45px
@@ -0,0 +1,16 @@
<template name="confirmDeleteDialog">
<div id="confirmDeleteDialog" class="dialog-animated" 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>
@@ -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();
}
}
});
@@ -0,0 +1,25 @@
#confirmDeleteDialog
position: absolute
top: 0
bottom: 0
left: 0
right: 0
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
.btn
text-decoration: none
#cancel
float: left
#confirm
float: right
@@ -0,0 +1,16 @@
<template name="displaySetNavigation">
<div id="{{this.id}}"
class="toolbarSectionButton rp-x-1 {{this.classes}} {{#if isNext}}js-next{{else}}js-prev{{/if}} {{#if (disableButton isNext)}}disabled{{/if}}"
title="{{this.title}}">
<div class="svgContainer">
{{#if isNext}}
<i class="fa fa-toggle-down fa-fw"></i>
{{else}}
<i class="fa fa-toggle-up fa-fw"></i>
{{/if}}
</div>
<div class="buttonLabel">
{{#if isNext}}Next{{else}}Previous{{/if}}
</div>
</div>
</template>
@@ -0,0 +1,27 @@
import { OHIF } from 'meteor/ohif:core';
import { Template } from 'meteor/templating';
Template.displaySetNavigation.events({
'click .js-next'(event, instance) {
if ($(event.currentTarget).hasClass('disabled')) {
return;
}
OHIF.viewer.moveDisplaySets(true);
},
'click .js-prev'(event, instance) {
if ($(event.currentTarget).hasClass('disabled')) {
return;
}
OHIF.viewer.moveDisplaySets(false);
}
});
Template.displaySetNavigation.helpers({
disableButton(isNext) {
Session.get('LayoutManagerUpdated');
return !OHIF.viewer.canMoveDisplaySets(isNext);
}
});
@@ -0,0 +1,11 @@
<template name="gridLayout">
<div id='imageViewerViewports'>
{{#each viewport in viewports}}
<div class="viewportContainer" style="height:{{height}}%;width:{{width}}%;">
<div class="removable">
{{>imageViewerViewport (clone viewport)}}
</div>
</div>
{{/each}}
</div>
</template>
@@ -0,0 +1,47 @@
import { OHIF } from 'meteor/ohif:core';
import { Template } from 'meteor/templating';
Template.gridLayout.helpers({
// Get the height percentage for each viewport
height() {
const instance = Template.instance();
const rows = instance.data.rows || 1;
return 100 / rows;
},
// Get the width percentage for each viewport
width() {
const instance = Template.instance();
const columns = instance.data.columns || 1;
return 100 / columns;
},
// Return the viewports list
viewports() {
const instance = Template.instance();
const rows = instance.data.rows;
const columns = instance.data.columns;
const numViewports = rows * columns;
const viewportData = instance.data.viewportData;
const numViewportsWithData = viewportData.length;
// Check if the viewportData length is different from the given
if (numViewportsWithData < numViewports) {
// Add the missing viewports
var difference = numViewports - numViewportsWithData;
for (var i = 0; i < difference; i++) {
viewportData.push({
viewportIndex: numViewportsWithData + i + 1,
rows,
columns
});
}
} else if (numViewportsWithData > numViewports) {
// Remove the additional viewports
return viewportData.slice(0, numViewports);
}
// Return the viewports
return viewportData;
}
});
@@ -0,0 +1,7 @@
@import "{design}/app.styl"
#imageViewerViewports
height: 100%
padding-bottom: 1px
padding-right: 6px
width: 100%
@@ -0,0 +1,11 @@
<template name="imageControls">
<div class="imageControls">
<div id="scrollbar">
<input id="imageSlider"
type="range"
min="1"
value="{{imageIndex}}"
max="{{numImages}}"/>
</div>
</div>
</template>
@@ -0,0 +1,57 @@
const slideTimeoutTime = 40;
let slideTimeout;
Template.imageControls.onRendered(() => {
const instance = Template.instance();
Meteor.defer(() => {
// Set the current imageSlider width to its parent's height
// (because webkit is stupid and can't style vertical sliders)
const $slider = instance.$('#imageSlider');
const $element = $slider.parents().eq(2).siblings('.imageViewerViewport');
const viewportHeight = $element.height();
$slider.width(viewportHeight - 20);
});
})
Template.imageControls.events({
'keydown #imageSlider'(event) {
// We don't allow direct keyboard up/down input on the
// image sliders since the natural direction is reversed (0 is at the top)
// Prevent the browser's default behaviour (scrolling)
event.preventDefault();
// Store the KeyCodes in an object for readability
const keys = {
DOWN: 40,
UP: 38
};
if (event.which === keys.DOWN) {
OHIF.viewer.hotkeyFunctions.scrollDown();
} else if (event.which === keys.UP) {
OHIF.viewer.hotkeyFunctions.scrollUp();
}
},
'input #imageSlider, change #imageSlider'(event) {
// Note that we throttle requests to prevent the
// user's ultrafast scrolling from firing requests too quickly.
clearTimeout(slideTimeout);
slideTimeout = setTimeout(() => {
// Using the slider in an inactive viewport
// should cause that viewport to become active
const slider = $(event.currentTarget);
const newActiveElement = slider.parents().eq(2).siblings('.imageViewerViewport').get(0);
setActiveViewport(newActiveElement);
// Subtract 1 here since the slider goes from 1 to N images
// But the stack indexing starts at 0
const newImageIdIndex = parseInt(slider.val(), 10) - 1;
switchToImageByIndex(newImageIdIndex);
}, slideTimeoutTime);
return false;
}
});
@@ -0,0 +1,115 @@
@import "{design}/app"
$imageSliderBorderRadius = 57px
$imageSliderTrackColor = rgba(0,0,0,0)
$imageSliderBorder = none
// Note that these are backwards due to the magic needed to make a cross-browser vertical slider
$imageSliderWidth = 12px
$imageSliderHeight = 39px
$imageSliderCursor = grab
.imageControls
position: absolute
top: 0
right: 0
height: 100%
padding: 5px
#scrollbar
height: calc(100% - 20px);
margin-top: 5px
width: 31px
#imageSlider
position: absolute
left: 20px
vendorize(transform, rotate(90deg))
vendorize(transform-origin, top left)
-webkit-appearance: none
background-color: $imageSliderTrackColor
// Remove focus highlights on range input
&:focus
outline: none
vendorize(box-shadow, none)
// Remove focus border in Firefox
&::-moz-focus-outer
border: none
// --- Style the range track --- //
&::-webkit-slider-runnable-track
height: 5px
border: none
cursor: pointer
background-color: $imageSliderTrackColor
z-index: 6
&::-moz-range-track
height: 2px
border: none
cursor: pointer
background-color: $imageSliderTrackColor
z-index: 6
&::-ms-track
width: 100%
height: 2px
border: none
cursor: pointer
animate: 0.2s
background: transparent
border-width: 15px 0
color: $imageSliderTrackColor
// Hide any fill IE tries to add
&::-ms-fill-lower
background: $imageSliderTrackColor
&::-ms-fill-upper
background: $imageSliderTrackColor
// --- Style the range thumb --- //
&::-webkit-slider-thumb
-webkit-appearance: none !important
theme('background-color', '$imageSliderColor')
border-radius: $imageSliderBorderRadius
border: $imageSliderBorder
height: $imageSliderWidth
width: $imageSliderHeight
cursor: $imageSliderCursor
margin-top: -7px
&:active
theme('background-color', '$activeColor')
&::-moz-range-thumb
theme('background-color', '$imageSliderColor')
border-radius: $imageSliderBorderRadius
border: $imageSliderBorder
height: $imageSliderWidth
width: $imageSliderHeight
cursor: $imageSliderCursor
z-index: 7
&:active
theme('background-color', '$activeColor')
&::-ms-thumb
theme('background-color', '$imageSliderColor')
border-radius: $imageSliderBorderRadius
border: $imageSliderBorder
height: $imageSliderWidth
width: $imageSliderHeight
cursor: $imageSliderCursor
&:active
theme('background-color', '$activeColor')
// Set left position in IE, border-width attribute breaks left position of imageSlider
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none)
#imageSlider
left: 50px
@@ -0,0 +1,14 @@
<template name="imageViewerViewport">
<div class='imageViewerViewport'
oncontextmenu='return false;'
unselectable='on'
onselectstart='return false;'
tabindex='0'>
</div>
<div class='viewportInstructions'>
Please drag a stack here to view images.
</div>
{{>loadingIndicator}}
{{>viewportOverlay}}
{{>viewportOrientationMarkers}}
</template>
@@ -0,0 +1,550 @@
import { OHIF } from 'meteor/ohif:core';
var allCornerstoneEvents = 'CornerstoneToolsMouseDown CornerstoneToolsMouseDownActivate ' +
'CornerstoneToolsMouseClick CornerstoneToolsMouseDrag CornerstoneToolsMouseUp ' +
'CornerstoneToolsMouseWheel CornerstoneToolsTap CornerstoneToolsTouchPress ' +
'CornerstoneToolsTouchStart CornerstoneToolsTouchStartActive ' +
'CornerstoneToolsMultiTouchDragStart';
/**
* This function loads a study series into a viewport element.
*
* @param data {object} Object containing the study, series, and viewport element to be used
*/
function loadDisplaySetIntoViewport(data, templateData) {
log.info('imageViewerViewport loadDisplaySetIntoViewport');
// Make sure we have all the data required to render the series
if (!data.study || !data.displaySet || !data.element) {
log.warn('loadDisplaySetIntoViewport: No Study, Display Set, or Element provided');
return;
}
// Get the current element and it's index in the list of all viewports
// The viewport index is often used to store information about a viewport element
var element = data.element;
var viewportIndex = $('.imageViewerViewport').index(element);
let layoutManager = window.layoutManager;
layoutManager.viewportData = layoutManager.viewportData || {};
layoutManager.viewportData[viewportIndex] = layoutManager.viewportData[viewportIndex] || {};
layoutManager.viewportData[viewportIndex].viewportIndex = viewportIndex;
// Get the contentID of the current study list tab, if the viewport is running
// alongside the study list package
var contentId = Session.get('activeContentId');
// If the viewer is inside a tab, create an object related to the specified viewport
// This data will be saved so that the tab can be reloaded to the same state after tabs
// are switched
if (contentId) {
if (!ViewerData[contentId]) {
return;
}
ViewerData[contentId].loadedSeriesData[viewportIndex] = {};
}
// Create an empty array to populate with image IDs
var imageIds = [];
// Loop through the current series and add metadata to the
// Cornerstone meta data provider. This will be used to fill information
// into the viewport overlays, and to calculate reference lines and orientation markers
var displaySet = data.displaySet;
var numImages = displaySet.images.length;
var imageId;
displaySet.images.forEach(function(image, imageIndex) {
var metaData = {
instance: image,
series: displaySet, // TODO: Check this
study: data.study,
numImages: numImages,
imageIndex: imageIndex + 1
};
var numFrames = image.numFrames;
if (numFrames > 1) {
log.info('Multiframe image detected');
for (var i = 0; i < numFrames; i++) {
metaData.frame = i;
imageId = getImageId(image, i);
imageIds.push(imageId);
addMetaData(imageId, metaData);
}
} else {
imageId = getImageId(image);
imageIds.push(imageId);
addMetaData(imageId, metaData);
}
});
// Define the current image stack using the newly created image IDs
var stack = {
currentImageIdIndex: data.currentImageIdIndex || 0,
imageIds: imageIds
};
// Get the current image ID for the stack that will be rendered
imageId = imageIds[stack.currentImageIdIndex];
// Save the current image ID inside the template data so it can be
// retrieved from the template helpers
templateData.imageId = imageId;
// Save the current image ID inside the ViewportLoading object.
//
// The ViewportLoading object relates the viewport elements with whichever
// image is currently being loaded into them. This is useful so that we can
// place progress (download %) for each image inside the proper viewports.
ViewportLoading[viewportIndex] = imageId;
// Enable Cornerstone for the viewport element
//
var options = {
renderer: 'webgl'
};
// NOTE: This uses the experimental WebGL renderer for Cornerstone!
// If you have problems, replace it with this line instead:
// cornerstone.enable(element);
cornerstone.enable(element, options);
// Get the handler functions that will run when loading has finished or thrown
// an error. These are used to show/hide loading / error text boxes on each viewport.
var endLoadingHandler = cornerstoneTools.loadHandlerManager.getEndLoadHandler();
var errorLoadingHandler = cornerstoneTools.loadHandlerManager.getErrorLoadingHandler();
// Get the current viewport settings
var viewport = cornerstone.getViewport(element);
// Store the current series data inside the Layout Manager
layoutManager.viewportData[viewportIndex] = {
imageId: imageId,
studyInstanceUid: data.studyInstanceUid,
seriesInstanceUid: data.seriesInstanceUid,
displaySetInstanceUid: data.displaySetInstanceUid,
currentImageIdIndex: data.currentImageIdIndex,
viewport: viewport,
viewportIndex: viewportIndex
};
// Start loading the image.
cornerstone.loadAndCacheImage(imageId).then(function(image) {
var enabledElement;
try {
enabledElement = cornerstone.getEnabledElement(element);
} catch (error) {
log.warn('Viewport destroyed before loaded image could be displayed');
return;
}
// 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);
// Update the metaData for missing fields
updateMetaData(image);
// Check if there are default viewport settings for this sopClassUid
if (!displaySet.images || !displaySet.images.length) {
return;
}
var instance = displaySet.images[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
// This will stop the loading percentage complete from being displayed.
delete ViewportLoading[viewportIndex];
// Call the handler function that represents the end of the image loading phase
// (e.g. hide the progress text box)
endLoadingHandler(element);
// Remove the 'empty' class from the viewport to hide any instruction text
element.classList.remove('empty');
// Hide the viewport instructions (i.e. 'Drag a stack here') and show
// the viewport overlay data.
$(element).siblings('.viewportInstructions').hide();
$(element).siblings('.imageViewerViewportOverlay').show();
// Add stack state managers for the stack tool, CINE tool, and reference lines
cornerstoneTools.addStackStateManager(element, ['stack', 'playClip', 'referenceLines']);
// Enable orientation markers, if applicable
updateOrientationMarkers(element);
// Clear any old stack data
cornerstoneTools.clearToolState(element, 'stack');
cornerstoneTools.addToolState(element, 'stack', stack);
// Set the default CINE settings
var multiframeMetadata = instance.multiframeMetadata;
var cineToolData = {
loop: OHIF.viewer.cine.loop,
framesPerSecond: multiframeMetadata ? multiframeMetadata.averageFrameRate : OHIF.viewer.cine.framesPerSecond
};
cornerstoneTools.addToolState(element, 'playClip', cineToolData);
// Autoplay datasets that have framerates set
if (multiframeMetadata && multiframeMetadata.isMultiframeImage && multiframeMetadata.averageFrameRate > 0) {
cornerstoneTools.playClip(element);
}
// Enable mouse, mouseWheel, touch, and keyboard input on the element
cornerstoneTools.mouseInput.enable(element);
cornerstoneTools.touchInput.enable(element);
cornerstoneTools.mouseWheelInput.enable(element);
cornerstoneTools.keyboardInput.enable(element);
// Use the tool manager to enable the currently active tool for this
// newly rendered element
var activeTool = toolManager.getActiveTool();
toolManager.setActiveTool(activeTool, [element]);
// Define a function to run whenever the Cornerstone viewport is rendered
// (e.g. following a change of window or zoom)
function onImageRendered(e, eventData) {
log.info('imageViewerViewport onImageRendered');
if (!layoutManager.viewportData[viewportIndex]) {
log.warn("onImageRendered: LayoutManager has no viewport data for this viewport index?: " + viewportIndex);
}
// Use Session to trigger reactive updates in the viewportOverlay helper functions
// This lets the viewport overlay always display correct window / zoom values
Session.set('CornerstoneImageRendered' + viewportIndex, Random.id());
// 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);
}
// Attach the onImageRendered callback to the CornerstoneImageRendered event
$(element).off('CornerstoneImageRendered', onImageRendered);
$(element).on('CornerstoneImageRendered', onImageRendered);
// Set a random value for the Session variable in order to trigger an overlay update
Session.set('CornerstoneImageRendered' + viewportIndex, Random.id());
// Define a function to run whenever the Cornerstone viewport changes images
// (e.g. during scrolling)
function onNewImage(e, eventData) {
log.info('imageViewerViewport onNewImage');
// Update the metaData for missing fields
updateMetaData(eventData.enabledElement.image);
// Update the templateData with the new imageId
// 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;
var toolData = cornerstoneTools.getToolState(element, 'stack');
if (!toolData || !toolData.data || !toolData.data.length) {
return;
}
var stack = toolData.data[0];
// If this viewport is displaying a stack of images, save the current image
// index in the stack to the global ViewerData object, as well as the Meteor Session.
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);
}
}
// Attach the onNewImage callback to the CornerstoneNewImage event
$(element).off('CornerstoneNewImage', onNewImage);
$(element).on('CornerstoneNewImage', onNewImage);
// Set a random value for the Session variable in order to trigger an overlay update
Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
// Define a function to trigger an event whenever a new viewport is being used
// This is used to update the value of the "active viewport", when the user interacts
// with a new viewport element
function sendActivationTrigger(e, eventData) {
// Check if the current active viewport in the Meteor Session
// Is the same as the viewport in which the activation event was fired.
// If it was, no changes are necessary, so stop here.
var element = eventData.element;
var activeViewportIndex = Session.get('activeViewport');
var viewportIndex = $('.imageViewerViewport').index(element);
// Reset the focus, even if we don't need to re-enable reference lines or prefetching
$(element).focus();
if (viewportIndex === activeViewportIndex) {
return;
}
log.info('imageViewerViewport sendActivationTrigger');
// Otherwise, trigger an 'ActivateViewport' event to be handled by the Template event
// handler
eventData.viewportIndex = viewportIndex;
var customEvent = $.Event('ActivateViewport', eventData);
// Need to overwrite the type set in the original event
customEvent.type = 'ActivateViewport';
$(e.target).trigger(customEvent, eventData);
}
// Attach the sendActivationTrigger function to all of the Cornerstone interaction events
$(element).off(allCornerstoneEvents, sendActivationTrigger);
$(element).on(allCornerstoneEvents, sendActivationTrigger);
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);
// If it is, and reference lines are enabled, add this element to the global synchronizer
// that is used for updating reference lines, and enable reference lines for this viewport.
if (OHIF.viewer.refLinesEnabled && imagePlane && imagePlane.frameOfReferenceUID) {
OHIF.viewer.updateImageSynchronizer.add(element);
}
// Set the active viewport based on the Session variable
// This is done to ensure that the active element has the current
// focus, so that keyboard events are triggered.
if (viewportIndex === Session.get('activeViewport')) {
setActiveViewport(element);
}
// Run any renderedCallback that exists in the data context
if (data.renderedCallback && typeof data.renderedCallback === 'function') {
data.renderedCallback(element);
}
// Update the LayoutManagerUpdated session key
layoutManager.updateSession();
}, function(error) {
// If something goes wrong while loading the image, fire the error handler.
errorLoadingHandler(element, imageId, error);
});
}
/**
* This function sets the display set for the study and calls LoadDisplaySetIntoViewport function
*
* @param data includes study data
* @param displaySetInstanceUid Display set information which is loaded in Template
* @param templateData currentData of Template
*
*/
function setDisplaySet(data, displaySetInstanceUid, templateData) {
var study = data.study;
if (!study || !study.displaySets) {
throw 'Study does not exist or has no display sets';
}
study.displaySets.every(displaySet => {
if (displaySet.displaySetInstanceUid === displaySetInstanceUid) {
data.displaySet = displaySet;
return false;
}
return true;
});
// If we didn't find anything, stop here
if (!data.displaySet) {
throw 'Display set not found in specified study!';
}
// Otherwise, load pass the data object into loadSeriesIntoViewport
loadDisplaySetIntoViewport(data, templateData);
}
/**
* This function searches an object to return the keys that contain a specific value
*
* @param object {object} The object to be searched
* @param value The value to be found
*
* @returns {array} The keys for which the object has the specified value
*/
function getKeysByValue(object, value) {
// http://stackoverflow.com/questions/9907419/javascript-object-get-key-by-value
return Object.keys(object).filter(function(key) {
return object[key] === value;
});
}
Meteor.startup(function() {
// On Meteor startup, define the global objects used to store loading imageIds
// by viewport / thumbnail element
ViewportLoading = {};
ThumbnailLoading = {};
// Whenever the CornerstoneImageLoadProgress is fired, identify which viewports
// the "in-progress" image is to be displayed in. Then pass the percent complete
// via the Meteor Session to the other templates to be displayed in the relevant viewports.
$(cornerstone).on('CornerstoneImageLoadProgress', function(e, eventData) {
viewportIndices = getKeysByValue(ViewportLoading, eventData.imageId);
viewportIndices.forEach(function(viewportIndex) {
Session.set('CornerstoneLoadProgress' + viewportIndex, eventData.percentComplete);
});
thumbnailIndices = getKeysByValue(ThumbnailLoading, eventData.imageId);
thumbnailIndices.forEach(function(thumbnailIndex) {
Session.set('CornerstoneThumbnailLoadProgress' + thumbnailIndex, eventData.percentComplete);
});
});
var config = {
magnifySize: 300,
magnificationLevel: 3
};
cornerstoneTools.magnify.setConfiguration(config);
});
Template.imageViewerViewport.onRendered(function() {
var templateData = Template.currentData();
log.info('imageViewerViewport onRendered');
// When the imageViewerViewport template is rendered
var element = this.find('.imageViewerViewport');
// Display the loading indicator for this element
$(element).siblings('.imageViewerLoadingIndicator').css('display', 'block');
// Get the current active viewport index, if this viewport has the same index,
// add the CSS 'active' class to highlight this viewport.
var activeViewport = Session.get('activeViewport');
// Create a data object to pass to the series loading function (loadSeriesIntoViewport)
var data = {
element: element,
viewport: this.data.viewport,
currentImageIdIndex: this.data.currentImageIdIndex,
displaySetInstanceUid: this.data.displaySetInstanceUid,
studyInstanceUid: this.data.studyInstanceUid,
seriesInstanceUid: this.data.seriesInstanceUid,
renderedCallback: this.data.renderedCallback,
activeViewport: activeViewport
};
// If no displaySetInstanceUid was supplied, display the drag/drop
// instructions and then stop here since we don't know what to display in the viewport.
if (!this.data.displaySetInstanceUid) {
element.classList.add('empty');
$(element).siblings('.imageViewerLoadingIndicator').css('display', 'none');
$(element).siblings('.viewportInstructions').show();
return;
}
// Look through the ViewerStudies collection for a
// study with this studyInstanceUid
var study = ViewerStudies.findOne({
studyInstanceUid: this.data.studyInstanceUid
});
var displaySetInstanceUid = this.data.displaySetInstanceUid;
data.study = study;
setDisplaySet(data, displaySetInstanceUid, templateData);
});
Template.imageViewerViewport.onDestroyed(function() {
log.info('imageViewerViewport onDestroyed');
// When a viewport element is being destroyed
var element = this.find('.imageViewerViewport');
if (!element || !$(element).find('canvas').length) {
return;
}
// Try to stop any currently playing clips
// Otherwise the interval will continuously throw errors
try {
var enabledElement = cornerstone.getEnabledElement(element);
if (enabledElement) {
cornerstoneTools.stopClip(element);
}
} catch (error) {
log.warn(error);
}
// Disable the viewport element with Cornerstone
// This also triggers the removal of the element from all available
// synchronizers, such as the one used for reference lines.
cornerstone.disable(element);
});
Template.imageViewerViewport.events({
'ActivateViewport .imageViewerViewport'(event) {
log.info('imageViewerViewport ActivateViewport');
setActiveViewport(event.currentTarget);
},
'click .imageViewerViewport'(event) {
setActiveViewport(event.currentTarget);
},
'CornerstoneToolsMouseDoubleClick .imageViewerViewport, CornerstoneToolsDoubleTap .imageViewerViewport'(event) {
// Get the double clicked viewport index
const viewportIndex = $('.imageViewerViewport').index(event.currentTarget);
// Enlarge the double clicked viewport
layoutManager.toggleEnlargement(viewportIndex);
// Wait for DOM re-rendering and update the active viewport
Tracker.afterFlush(() => {
// Check if the viewer is zoomed
if (layoutManager.isZoomed) {
// Set the active viewport as the only one visible
setActiveViewport($('.imageViewerViewport')[0]);
} else {
// Set the active viewport as the previous zoomed viewport
setActiveViewport($('.imageViewerViewport').eq(window.layoutManager.zoomedViewportIndex));
}
});
}
});
@@ -0,0 +1,37 @@
@import "{design}/app.styl"
.imageViewerViewport
width: 100%
height: 100%
background-color: black
// Prevent the blue outline in Chrome when a viewport is selected
outline: 0 !important
// Prevents the entire page from getting larger
// when the magnify tool is near the sides/corners of the page
overflow: hidden
.viewportInstructions
display: none
font-size: 13px
theme('color', '$textSecondaryColor')
line-height: 18px
pointer-events: none // Necessary for drag/drop through to cornerstone element below
text-align: center
position: absolute
top: 0
bottom: 0
right: 0
left: 0
margin: auto
height: 20px
/* These are some good CSS settings for a circular magnifying glass
Try removing the border-radius to make a square magnifying glass */
.magnifyTool
border: 4px white solid
box-shadow: 2px 2px 10px #1e1e1e
border-radius: 50%
display: none
cursor: none
@@ -0,0 +1,20 @@
<template name="layoutButton">
<div id="{{this.id}}"
class="toolbarSectionButton rp-x-1 {{this.classes}} {{activeClass}} js-dropdown-toggle"
title="{{this.title}}"
data-target="#layoutChooser"
>
<div class="svgContainer">
{{#if this.svgLink}}
<svg>
<use xlink:href={{this.svgLink}}></use>
</svg>
{{else}}
<i class={{iconClasses}}></i>
{{/if}}
</div>
<div class="buttonLabel">
{{this.title}}
</div>
</div>
</template>
@@ -0,0 +1,17 @@
Template.layoutButton.events({
// TODO: Check why 'click' event won't fire?
'mousedown .js-dropdown-toggle'(event) {
// Select the button and it's target dropdown menu
const $button = $(event.currentTarget);
const $dropdown = $($button.data('target'));
// Adjust the dropdown's CSS to properly place it on the page
$dropdown.css({
top: $button.offset().top + $button.outerHeight() + 'px',
left: $button.offset().left + 'px'
});
// Open or close the layout chooser dialog
toggleDialog($dropdown);
}
});
@@ -0,0 +1,30 @@
<template name="layoutChooser">
<div id="layoutChooser" class="layoutChooser dialog-animated">
<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>
</template>
@@ -0,0 +1,87 @@
Template.layoutChooser.onRendered(() => {
const instance = Template.instance();
/**
* Adds the 'hover' class to cells above and to the left of the current cell
* This is used to "fill in" the grid that the user will change the layout to,
* if they click on a specific table cell.
*
* @param currentCell
*/
instance.highlightCells = currentCell => {
const cells = this.$('.layoutChooser table td');
cells.removeClass('hover');
currentCell = this.$(currentCell);
const table = currentCell.parents('.layoutChooser table').get(0);
const rowIndex = currentCell.closest('tr').index();
const columnIndex = currentCell.index();
// Loop through the table row by row
// and cell by cell to apply the highlighting
for (let i = table.rows.length - 1; i >= 0; i--) {
const row = table.rows[i];
if (i <= rowIndex) {
for (let j = row.cells.length - 1; j >= 0; j--) {
if (j <= columnIndex) {
const cell = row.cells[j];
cell.classList.add('hover');
}
}
}
}
};
// Refresh layout chooser highlighting based on current viewports state
instance.refreshHighlights = () => {
// Stop here if layoutManager is not defined yet
if (!window.layoutManager) {
return;
}
// Get the layout rows and columns amount
const info = window.layoutManager.layoutProps;
// get the limiter cell
const cell = instance.$('tr').eq(info.rows - 1).children().eq(info.columns - 1);
// Highlight all cells before the limiter
instance.highlightCells(cell);
};
instance.autorun(() => {
// Run this computation every time the viewer layout is changed
Session.get('LayoutManagerUpdated');
instance.refreshHighlights();
});
});
Template.layoutChooser.events({
'touchstart .layoutChooser table td, mouseenter .layoutChooser table td'(event, instance) {
instance.highlightCells(event.currentTarget);
},
'mouseleave .layoutChooser'(event, instance) {
instance.refreshHighlights();
},
'click .layoutChooser table td'(event, instance) {
const $currentCell = instance.$(event.currentTarget);
const rowIndex = $currentCell.closest('tr').index();
const columnIndex = $currentCell.index();
// Add 1 because the indices start from zero
const layoutProps = {
rows: rowIndex + 1,
columns: columnIndex + 1
};
window.layoutManager.layoutTemplateName = 'gridLayout';
window.layoutManager.layoutProps = layoutProps;
window.layoutManager.updateViewports();
const $dropdown = instance.$('.layoutChooser');
toggleDialog($dropdown);
}
});
@@ -0,0 +1,28 @@
@import "{design}/app.styl"
$borderColor = rgba(77, 99, 110, 0.81)
.layoutChooser
theme('background', '$uiGrayDarkest', 0.95)
border: 1px solid $borderColor;
border-radius: 8px
height: 92px
min-height: 92px
min-width: 92px // to override bootstrap's dropdown-menu class
padding: 5px 0;
position: absolute;
width: 92px
z-index: 5000
table
margin: 0 auto
td
theme('border', '1px solid $uiBorderColorDark')
cursor: pointer
height: 20px
transition(background-color 0.1s ease)
width: 20px
&:hover, &.hover // Add the hover class here to be triggered by mouseenter/mouseleave
theme('background-color', '$activeColor')
@@ -0,0 +1,14 @@
<template name="loadingIndicator">
<div class="imageViewerLoadingIndicator loadingIndicator">
<div class="indicatorContents">
<p>Loading... <i class="fa fa-spin fa-circle-o-notch fa-fw"></i> {{percentComplete}}</p>
</div>
</div>
<div class="imageViewerErrorLoadingIndicator loadingIndicator">
<div class="indicatorContents">
<h4>Error Loading Image</h4>
<p class='description'>An error has occurred.</p>
<p class='details'></p>
</div>
</div>
</template>
@@ -0,0 +1,72 @@
import { OHIF } from 'meteor/ohif:core';
Meteor.startup(function() {
cornerstoneTools.loadHandlerManager.setStartLoadHandler(startLoadingHandler);
cornerstoneTools.loadHandlerManager.setEndLoadHandler(doneLoadingHandler);
cornerstoneTools.loadHandlerManager.setErrorLoadingHandler(errorLoadingHandler);
});
var loadHandlerTimeout;
startLoadingHandler = function(element) {
clearTimeout(loadHandlerTimeout);
loadHandlerTimeout = setTimeout(function() {
var elem = $(element);
elem.siblings('.imageViewerErrorLoadingIndicator').css('display', 'none');
elem.find('canvas').not('.magnifyTool').addClass("faded");
elem.siblings('.imageViewerLoadingIndicator').css('display', 'block');
}, OHIF.viewer.loadIndicatorDelay);
};
doneLoadingHandler = function(element) {
clearTimeout(loadHandlerTimeout);
var elem = $(element);
elem.siblings('.imageViewerErrorLoadingIndicator').css('display', 'none');
elem.find('canvas').not('.magnifyTool').removeClass("faded");
elem.siblings('.imageViewerLoadingIndicator').css('display', 'none');
};
errorLoadingHandler = function(element, imageId, error, source) {
clearTimeout(loadHandlerTimeout);
var elem = $(element);
// Could probably chain all of these, but this is more readable
elem.find('canvas').not('.magnifyTool').removeClass("faded");
elem.siblings('.imageViewerLoadingIndicator').css('display', 'none');
// Don't display errors from the stackPrefetch tool
if (source === "stackPrefetch") {
return;
}
var errorLoadingIndicator = elem.siblings('.imageViewerErrorLoadingIndicator');
errorLoadingIndicator.css('display', 'block');
var cleanedImageId = imageId;
// This is just used to expand upon some error messages that are sent
// when things fail. An example is a network error throwing the error
// which is only described as "network".
var errorDetails = {
network: "A network error has occurred"
// We need to expand this further when we see more obscure error messages
};
if (errorDetails.hasOwnProperty(error)) {
error = errorDetails[error];
}
errorLoadingIndicator.find('.description').text("An error has occurred while loading image: " + cleanedImageId);
if (error) {
errorLoadingIndicator.find('.details').text("Details: " + error);
}
};
Template.loadingIndicator.helpers({
'percentComplete'() {
var percentComplete = Session.get('CornerstoneLoadProgress' + this.viewportIndex);
if (percentComplete && percentComplete !== 100) {
return percentComplete + '%';
}
}
});
@@ -0,0 +1,39 @@
@import "{design}/app.styl"
.imageViewerLoadingIndicator
theme('color', '$textSecondaryColor')
.faded
opacity: 0.5
.imageViewerErrorLoadingIndicator
theme('color', '$uiYellow')
p, h4
padding: 4px 0
text-align: center
word-wrap: break-word
p
font-size: 11pt
.loadingIndicator
background-color: rgba(0,0,0,0.75)
display: none
font-size: 18px
height: 100%
overflow: hidden
pointer-events: none // Necessary for click-through to cornerstone element below
position: absolute
top: 0
width: 100%
z-index: 100
.indicatorContents
font-size: 30px
font-weight: 300
position: absolute
text-align: center
top: 50%
transform(translateY(-50%))
width: 100%
@@ -0,0 +1,16 @@
<template name="playClipButton">
<div id="{{this.id}}"
class="toolbarSectionButton rp-x-1 {{this.classes}} {{activeClass}} {{#if isPlaying}}active{{/if}} {{#if (disableButton)}}disabled{{/if}}"
title="{{this.title}}">
<div class="svgContainer">
{{#if isPlaying}}
<i class="fa fa-stop fa-fw"></i>
{{else}}
<i class="fa fa-play fa-fw"></i>
{{/if}}
</div>
<div class="buttonLabel">
{{#if isPlaying}}Stop{{else}}Play{{/if}}
</div>
</div>
</template>
@@ -0,0 +1,8 @@
Template.playClipButton.helpers({
isPlaying: function() {
return isPlaying();
},
disableButton() {
return hasMultipleFrames();
}
});
@@ -0,0 +1,42 @@
<template name="studySeriesQuickSwitch">
<div class="quickSwitchWrapper {{side}}">
<div class="quickSwitch js-quick-switch clearfix rp-t-1 {{#if eq side 'right'}}rp-l-3{{/if}}{{#if eq side 'left'}}rp-r-3{{/if}}">
<div class="switchSection switchSectionStudy rp-x-1 {{#if eq side 'right'}}pull-left{{else}}pull-right{{/if}}">
<div class="label">Study</div>
<div class="studySwitch">
<div class="studyBox"></div>
<div class="switchHover studyHover clearfix">
<div class="scrollArea">
{{>studyTimepointBrowser (clone this timepointViewType='key')}}
</div>
</div>
</div>
</div>
<div class="switchSection switchSectionSeries rp-x-1 {{#if eq side 'right'}}pull-left{{else}}pull-right{{/if}}">
{{#let thumbnailsList=(studyThumbnails currentStudy)}}
<div class="label">Series</div>
<div class="seriesSwitch clearfix">
{{#each thumbnail in thumbnailsList}}
{{#if lt @index 8}}
<div class="seriesItem {{#if eq side 'right'}}pull-left{{else}}pull-right{{/if}} {{#if isDisplaySetActive thumbnail.stack.displaySetInstanceUid viewportIndex}}active{{/if}}"></div>
{{else}}
{{#if eq @index 8}}
<div class="seriesItem count {{#if eq side 'right'}}pull-left{{else}}pull-right{{/if}}">{{thumbnailsList.length}}</div>
{{/if}}
{{/if}}
{{/each}}
<div class="switchHover seriesHover clearfix">
<div class="scrollArea">
<div class="thumbnailsWrapper">
{{#each thumbnail in thumbnailsList}}
{{>thumbnailEntry (clone this thumbnail=thumbnail)}}
{{/each}}
</div>
</div>
</div>
</div>
{{/let}}
</div>
</div>
</div>
</template>
@@ -0,0 +1,45 @@
Template.studySeriesQuickSwitch.onCreated(() => {
const instance = Template.instance();
// Defines the study being shown in the current viewport
instance.data.currentStudy = new ReactiveVar();
// Gets the viewport data for the given viewport index
instance.getViewportData = viewportIndex => {
const layoutManager = window.layoutManager;
return layoutManager && layoutManager.viewportData && layoutManager.viewportData[viewportIndex];
};
// Gets the current viewport data
const viewportIndex = instance.data.viewportIndex;
const viewportData = instance.getViewportData(viewportIndex);
if (!viewportData) {
return;
}
// Finds the current study and return it
const study = ViewerStudies.findOne({
studyInstanceUid: viewportData.studyInstanceUid
});
// Change the current study to update the thumbnails
instance.data.currentStudy.set(study);
});
Template.studySeriesQuickSwitch.events({
'mouseenter .js-quick-switch, mouseenter .js-quick-switch .switchSectionSeries'(event, instance) {
instance.$('.quickSwitchWrapper').addClass('overlay');
$(event.currentTarget).addClass('hover');
},
'mouseleave .js-quick-switch'(event, instance) {
instance.$('.js-quick-switch, .switchSectionSeries').removeClass('hover');
instance.$('.quickSwitchWrapper').removeClass('overlay');
}
});
Template.studySeriesQuickSwitch.helpers({
// Get the current study
currentStudy() {
return Template.instance().data.currentStudy.get();
}
});
@@ -0,0 +1,249 @@
@import "{design}/app"
$switchSize = 55px
$seriesSpacing = 2px
.quickSwitchWrapper
position: fixed
top: 0
z-index: 1
&.middle
left: 50%
.quickSwitch
transform(translateX(-50%))
&.left
theme('border-right', '1px solid $uiBorderColor')
right: 50%
&.right
left: 50%
&.overlay .quickSwitch
transition(z-index 0s linear 0s)
z-index: 3
&:before
theme('background-color', '$primaryBackgroundColor')
bottom: 0
content: ''
left: 0
opacity: 0
position: fixed
right: 0
top: 0
transition(opacity 0.3s ease\, visibility 0s linear 0.3s)
visibility: hidden
z-index: 2
&.overlay:before
opacity: 0.8
transition(opacity 0.3s ease\, visibility 0s linear 0s)
visibility: visible
.quickSwitch
position: relative
transition(z-index 0s linear 0.3s)
z-index: 1
.switchSection
display: inline-block
float: left
height: 100%
position: relative
&.switchSectionSeries.hover, &.switchSectionStudy:hover
.seriesSwitch .seriesItem
opacity: 0
.switchHover
opacity: 1
.studyHover
height: auto
padding-top: $switchSize
transition(padding-top 0.3s ease\, visibility 0s linear 0s)
visibility: visible
&.switchSectionSeries.hover
.seriesHover
.thumbnailsWrapper
transform(scale(0.9) translateY($switchSize))
transition(all 0.3s ease)
.thumbnailEntry .seriesDetails
opacity: 0
visibility: hidden
&:hover .seriesHover
.thumbnailsWrapper
transform(scale(1) translateY(0))
.thumbnailEntry .seriesDetails
opacity: 1
transform(translateY(0))
transition(all 0.3s ease)
transition-delay(0.3s)
visibility: visible
.seriesHover .thumbnailEntry .seriesDetails
opacity: 0
transform(translateY(-36px))
.seriesSwitch .seriesItem
transition(opacity 0.3s ease)
.studySwitch, .seriesSwitch
position: relative
.studySwitch
.studyTimepointBrowser
background-color: transparent
.studyTimepointStudy.active .studyModality
theme('box-shadow', 'inset 0 0 0 3px $activeColor')
.studyBox
theme('background-color', '$uiGrayDark')
theme('border', 'solid 1px $activeColor')
border-radius: 11px
display: block
height: $switchSize
width: $switchSize
.seriesSwitch
display: block
height: $switchSize + $seriesSpacing
width: $switchSize + $seriesSpacing
.seriesItem
theme('background-color', '$boxBackgroundColor')Dark
border-radius: 3px
height: 15px
margin: $seriesSpacing
width: 15px
&.count
background-color: transparent
theme('color', '$textPrimaryColor')
font-size: 12px
font-weight: 500
line-height: 17px
text-align: center
&.active
theme('background-color', '$activeColor')
.label
theme('color', '$textSecondaryColor')
display: block
font-size: 12px
font-weight: 300
line-height: 34px
padding: 0
text-align: center
text-transform: uppercase
.switchHover
border: 10px solid transparent
border-radius: 5px
margin: -10px
opacity: 0
overflow: hidden
position: absolute
top: 0
z-index: 10000
.scrollArea
margin-right: -20px
min-height: 200px
max-height: 500px
overflow-x: hidden
overflow-y: scroll
width: calc(100% + 20px)
.studyHover
height: 0
padding-top: 0
transition(padding-top 0.3s ease\, visibility 0s linear 0.3s)
visibility: hidden
width: 320px
.scrollArea
height: 500px
.seriesHover
transform(scale(0))
transition(all 0.3s ease)
width: 731px
.thumbnailEntry
margin: 0
.quickSwitchWrapper.left, .quickSwitchWrapper.middle
.seriesHover .thumbnailEntry
float: right
.studyHover
left: 0
.seriesHover, .thumbnailsWrapper
right: 0
transform-origin(100% 0%)
.quickSwitchWrapper.right
.seriesHover .thumbnailEntry
float: left
.studyHover
right: 0
.seriesHover, .thumbnailsWrapper
left: 0
transform-origin(0% 0%)
// Responsive layout
$switchSmallSize = 35px
$seriesSmallSpacing = 1px
@media screen and (max-width: 1023px)
.switchSectionSeries.hover .seriesHover
transform(scale(0.5))
@media screen and (min-width: 1024px) and (max-width: 1151px)
.switchSectionSeries.hover .seriesHover
transform(scale(0.61))
@media screen and (min-width: 1152px) and (max-width: 1279px)
.switchSectionSeries.hover .seriesHover
transform(scale(0.69))
@media screen and (min-width: 1280px) and (max-width: 1359px)
.switchSectionSeries.hover .seriesHover
transform(scale(0.77))
@media screen and (min-width: 1360px) and (max-width: 1439px)
.switchSectionSeries.hover .seriesHover
transform(scale(0.82))
@media screen and (min-width: 1440px) and (max-width: 1599px)
.switchSectionSeries.hover .seriesHover
transform(scale(0.87))
@media screen and (min-width: 1600px) and (max-width: 1919px)
.switchSectionSeries.hover .seriesHover
transform(scale(0.97))
@media screen and (min-width: 1920px)
.switchSectionSeries.hover .seriesHover
transform(scale(1))
@media screen and (max-width: 1599px)
.quickSwitch
.switchSection
.label
font-size: 8px
font-weight: 400
line-height: 12px
padding: 0
.studySwitch .studyBox
border-radius: 7px
height: $switchSmallSize
width: $switchSmallSize
.seriesSwitch
height: $switchSmallSize + $seriesSmallSpacing
width: $switchSmallSize + $seriesSmallSpacing
.seriesItem
border-radius: 2px
height: 10px
margin: $seriesSmallSpacing
width: 10px
&.count
font-size: 10px
font-weight: 300
line-height: 10px
@@ -0,0 +1,12 @@
<template name="studyTimepoint">
<div class="studyTimepointWrapper">
<div class="studyTimepoint">
{{#each study in studies}}
{{>studyTimepointStudy (clone this
study=study
active=(isActive study)
)}}
{{/each}}
</div>
</div>
</template>
@@ -0,0 +1,90 @@
Template.studyTimepoint.onCreated(() => {
const instance = Template.instance();
instance.isActive = {};
});
// Initialize the timepoint wrapper max-height to enable CSS transition
Template.studyTimepoint.onRendered(() => {
const instance = Template.instance();
const $studies = instance.$('.studyTimepoint');
const $wrapper = $studies.closest('.studyTimepointWrapper');
const $timepoint = $wrapper.closest('.timepointEntry');
const studiesVisible = $studies.is(':visible');
if (!studiesVisible) {
$timepoint.addClass('active');
}
// Recalculates the timepoint height to make CSS transition smoother
$studies.trigger('displayStateChanged');
if (!studiesVisible) {
$timepoint.removeClass('active');
}
});
Template.studyTimepoint.events({
// Changes the selected study
'selectionChanged .studyTimepoint'(event, instance, changed) {
const $selection = $(changed.selection);
// Defines where will be the studies searched
let $studiesTarget = instance.$('.studyTimepoint');
if (changed.isQuickSwitch) {
// Changes the current quick switch study
const study = ViewerStudies.findOne({
studyInstanceUid: changed.studyInstanceUid
});
instance.data.currentStudy.set(study);
// Changes the target to toggle the selection in all the studies
$studiesTarget = $studiesTarget.closest('.studyTimepointBrowser');
}
// Removes selected state from all studies but the triggered study
$studiesTarget.find('.studyTimepointStudy').not($selection).removeClass('active');
if (changed.isQuickSwitch) {
// Reset active studies map to allow only one active study
instance.isActive = {};
// Add selected state for the triggered study
$selection.addClass('active');
} else {
const $timepoint = instance.$('.studyTimepoint');
// Set the max-height to inherit to be able to expand the wrapper on its full height
instance.$('.studyTimepointWrapper').css('max-height', 'inherit');
// Toggle selected state for the triggered study
$selection.removeClass('loading');
$selection.toggleClass('active');
// Recalculates the timepoint height to make CSS transition smoother
const $thumbnails = $selection.find('.studyTimepointThumbnails');
$thumbnails.one('transitionend', () => $timepoint.trigger('displayStateChanged'));
}
// Set the current study as active
instance.isActive[changed.studyInstanceUid] = $selection.hasClass('active');
},
// It should be triggered when the timepoint height is changed
'displayStateChanged .studyTimepoint'(event, instance) {
const $timepoint = $(event.currentTarget);
const $wrapper = $timepoint.closest('.studyTimepointWrapper');
// Set the max-height for the wrapper to make CSS transition smoother
$wrapper.css('max-height', $timepoint.height());
}
});
Template.studyTimepoint.helpers({
isActive(study) {
const instance = Template.instance();
if (!study.studyInstanceUid) {
return;
}
return instance.isActive[study.studyInstanceUid];
}
});
@@ -0,0 +1,8 @@
@import "{design}/app"
.studyTimepointWrapper
overflow: hidden
transition($sidebarTransition)
.timepointEntry:not(.active) .studyTimepointWrapper
max-height: 0 !important
@@ -0,0 +1,42 @@
<template name="studyTimepointBrowser">
{{#let timepointList=timepoints}}
<div class="studyTimepointBrowser noselect {{#if shallShowViewType timepointList}}viewTypeVisible{{/if}}">
<div class="timepointButtonContainer p-t-2">
{{>roundedButtonGroup viewTypeButtonGroupData}}
</div>
<div class="studyTimepointScrollArea">
<div class="p-l-1">
{{#if timepointList.length}}
{{#each timepoint in timepointList}}
<div class="timepointEntry p-y-2">
<div class="timepointHeader">
<div class="timepointDetails clearfix">
<div class="timepointFollowupTitle pull-left">
{{timepointApi.title timepoint}}
</div>
<div class="expandIcon pull-right">
<i class="fa fa-chevron-down"></i>
</div>
<div class="timepointDate pull-right m-r-1">{{formatDA timepoint.earliestDate 'D-MMM-YYYY'}}</div>
</div>
<div class="timepointModalities">{{modalitiesSummary timepoint}}</div>
</div>
{{>studyTimepoint studies=(studies timepoint) index=@index viewportIndex=this.viewportIndex currentStudy=this.currentStudy}}
</div>
<hr>
{{/each}}
{{#if and this.currentStudy (not showAdditionalTimepoints)}}
<div class="studyModality additional">
Show additional timepoints
</div>
{{/if}}
{{else}}
<div class="p-t-2">
{{>studyTimepoint (clone this studies=studies)}}
</div>
{{/if}}
</div>
</div>
</div>
{{/let}}
</template>
@@ -0,0 +1,165 @@
Template.studyTimepointBrowser.onCreated(() => {
const instance = Template.instance();
// Reactive variable to control the view type for all or key timepoints
instance.timepointViewType = new ReactiveVar(instance.data.timepointViewType);
// Defines whether to show all key timepoints or only the current one
instance.showAdditionalTimepoints = new ReactiveVar(true);
// Return the current study if it's defined
instance.getCurrentStudy = () => {
return instance.data.currentStudy && instance.data.currentStudy.get();
};
// Get the studies for a specific timepoint
instance.getStudies = timepoint => {
if (!timepoint) {
return ViewerStudies.find().fetch();
}
return timepoint.studyInstanceUids.map(studyInstanceUid => {
const query = {
patientId: timepoint.patientId,
studyInstanceUid: studyInstanceUid
};
var loadedStudy = ViewerStudies.findOne(query);
if (loadedStudy) {
return loadedStudy;
}
var notYetLoaded = StudyListStudies.findOne(query);
if (!notYetLoaded) {
throw 'No study data available for Study: ' + studyInstanceUid;
}
return notYetLoaded;
});
};
});
Template.studyTimepointBrowser.onRendered(() => {
const instance = Template.instance();
instance.autorun(() => {
// Runs this computation every time the timepointViewType is changed
const type = instance.timepointViewType.get();
// Removes all active classes to collapse the timepoints and studies
instance.$('.timepointEntry, .studyTimepointStudy').removeClass('active');
if (type === 'key') {
// Show only first timepoint expanded for key timepoints
instance.$('.timepointEntry:first').addClass('active');
}
});
let lastStudy;
instance.autorun(() => {
// Runs this computation every time the curenty study is changed
const currentStudy = instance.data.currentStudy && instance.data.currentStudy.get();
// Check if the study really changed and update the last study
if (currentStudy !== lastStudy) {
instance.showAdditionalTimepoints.set(false);
lastStudy = currentStudy;
}
});
});
Template.studyTimepointBrowser.events({
'click .timepointHeader'(event, instance) {
const $timepoint = $(event.currentTarget).closest('.timepointEntry');
// Recalculates the timepoint height to make CSS transition smoother
$timepoint.find('.studyTimepoint').trigger('displayStateChanged');
// Toggle active class to group/ungroup timepoint studies
$timepoint.toggleClass('active');
},
'click .studyModality.additional'(event, instance) {
// Show all key timepoints
instance.showAdditionalTimepoints.set(true);
}
});
Template.studyTimepointBrowser.helpers({
// Decides if the timepoint view type switch shall be shown or omitted
shallShowViewType(timepointList) {
const instance = Template.instance();
return timepointList.length && !instance.data.timepointViewType;
},
// Returns the button group data for switching between timepoint view types
viewTypeButtonGroupData() {
return {
value: Template.instance().timepointViewType,
options: [{
value: 'key',
text: 'Key Timepoints'
}, {
value: 'all',
text: 'All Timepoints'
}]
};
},
// Defines whether to show all key timepoints or only the current one
showAdditionalTimepoints() {
return Template.instance().showAdditionalTimepoints.get();
},
// Get the timepoints to be listed
timepoints() {
const instance = Template.instance();
// Get the current study
const currentStudy = instance.getCurrentStudy();
// Declare the timepoints
let timepoints;
if (currentStudy && !instance.showAdditionalTimepoints.get()) {
// Show only the current study's timepoint
timepoints = instance.data.timepointApi.study(currentStudy.studyInstanceUid);
} else {
if (!instance.data.timepointApi) {
// If there is no timepoint API defined whatsoever, this means that there is no
// current timepoint ID, so we can just display all of the currently loaded studies
// in the study sidebar
timepoints = [];
} else if (instance.timepointViewType.get() === 'all') {
// Show all timepoints
timepoints = instance.data.timepointApi.all();
} else {
// Show only key timepoints
timepoints = instance.data.timepointApi.key();
}
}
// Returns the timepoints
return timepoints;
},
// Get the studies for a specific timepoint
studies(timepoint) {
return Template.instance().getStudies(timepoint);
},
// Build the modalities summary for all timepoint's studies
modalitiesSummary(timepoint) {
const instance = Template.instance();
const studies = instance.getStudies(timepoint);
const modalities = {};
studies.forEach(study => {
const modality = study.modalities || 'UN';
modalities[modality] = modalities[modality] + 1 || 1;
});
const result = [];
_.each(modalities, (count, modality) => {
result.push(`${count} ${modality}`);
});
return result.join(', ');
}
});
@@ -0,0 +1,104 @@
@import "{design}/app"
$timepointButtonHeight = 55px
.studyTimepointBrowser
theme('background-color', '$primaryBackgroundColor')
float: left
height: 100%
position: relative
width: 100%
.timepointButtonContainer
theme('background-color', '$primaryBackgroundColor')
theme('border-bottom', '1px solid $uiBorderColor')
display: none
height: $timepointButtonHeight
left: 10px
position: absolute
right: 10px
top: 0
z-index: 1
&.viewTypeVisible
.timepointButtonContainer
display: block
.studyTimepointScrollArea
padding-top: $timepointButtonHeight
.timepointEntry
&.active
.timepointModalities
height: 0
.timepointDate
display: none
.expandIcon i
transform: rotateX(180deg)
.timepointFollowupTitle
font-size: 14px
theme('color', '$textSecondaryColor')
text-transform: uppercase
.timepointHeader
cursor: pointer
font-size: 13px
&
.expandIcon i
.timepointFollowupTitle
.timepointDate
.timepointModalities
theme('color', '$textSecondaryColor')
transition($sidebarTransition)
&:hover
&
.timepointFollowupTitle
.expandIcon i
.timepointDate
.timepointModalities
theme('color', '$textPrimaryColor') !important
.timepointDetails
.timepointModalities
line-height: 25px
.timepointModalities
height: 25px
overflow: hidden
.expandIcon i
theme('color', '$defaultColor')
.timepointFollowupTitle
theme('color', '$textPrimaryColor')
padding-top: 2px
.studyTimepointScrollArea
height: 100%
overflow-x: hidden
overflow-y: auto
margin-right: -22px
padding-bottom: 20px
padding-right: 16px
&::-webkit-scrollbar
display: none
hr, .timepointHeader, .studyItem, .studyModality.additional
margin-right: 16px
.studySidebarTimepoint .thumbnailEntry.active:before
theme('background-color', '$activeColor')
border-top-right-radius: 7px
border-bottom-right-radius: 7px
content: ''
display: block
height: 91px
pointer-events: none
position: fixed
right: 0
top: 0
width: 5px
@@ -0,0 +1,31 @@
<template name="studyTimepointStudy">
{{#let isSidebar=(isUndefined viewportIndex)}}
<div class="studyTimepointStudy {{#if isSidebar}}studySidebarTimepoint{{else}}studyQuickSwitchTimepoint{{/if}} {{#if this.active}}active{{/if}}">
<div class="studyItem">
{{>loadingText}}
<div class="studyModality">
<div class="studyModalityBox">
<div class="studyModalityText" style="{{modalityStyle}}">
{{#if this.study.modalities}}
{{ modalities }}
{{else}}
UN
{{/if}}
</div>
</div>
<div class="studyText">
<div class="studyDate">{{formatDA this.study.studyDate 'D-MMM-YYYY'}}</div>
<div class="studyDescription">{{this.study.studyDescription}}</div>
</div>
</div>
</div>
{{#if isSidebar}}
<div class="studyTimepointThumbnails">
{{#each thumbnail in (studyThumbnails this.study)}}
{{>thumbnailEntry (clone this thumbnail=thumbnail)}}
{{/each}}
</div>
{{/if}}
</div>
{{/let}}
</template>
@@ -0,0 +1,113 @@
Template.studyTimepointStudy.onCreated(() => {
const instance = Template.instance();
// Set the current study as selected in the studies list
instance.select = (isQuickSwitch=false) => {
const $study = instance.$('.studyTimepointStudy');
const $timepoint = $study.closest('.studyTimepoint');
const selectionChanged = {
selection: [$study[0]],
studyInstanceUid: instance.data.study.studyInstanceUid,
isQuickSwitch
};
$timepoint.trigger('selectionChanged', selectionChanged);
};
});
// Initialize the study wrapper max-height to enable CSS transition
Template.studyTimepointStudy.onRendered(() => {
const instance = Template.instance();
const $study = instance.$('.studyTimepointStudy');
const $thumbnails = instance.$('.studyTimepointThumbnails');
$study.addClass('active');
$thumbnails.css('max-height', $thumbnails.height());
$study.removeClass('active');
// Here we add, remove, and add the active class again because this way
// the max-height animation appears smooth to the user.
if (instance.data.active) {
Meteor.setTimeout(() => {
$study.addClass('active');
}, 1);
}
});
Template.studyTimepointStudy.helpers({
modalities() {
const instance = Template.instance();
let modalities = instance.data.study.modalities;
// Replace backslashes with spaces
return modalities.replace(/\\/g, ' ');
},
modalityStyle() {
// Responsively styles the Modality Acronyms for studies
// with more than one modality
const instance = Template.instance();
const modalities = instance.data.study.modalities || 'UN';
const numModalities = modalities.split(/\\/g).length;
if (numModalities === 1) {
// If we have only one modality, it should take up the whole
// div.
return 'font-size: 1vw';
} else if (numModalities === 2) {
// If we have two, let them sit side-by-side
return 'font-size: 0.75vw';
} else {
// If we have more than two modalities, change the line
// height to display multiple rows, depending on the number
// of modalities we need to display.
const lineHeight = Math.ceil(numModalities / 2) * 1.2;
return 'line-height: ' + lineHeight + 'vh';
}
}
});
Template.studyTimepointStudy.events({
// Recalculates the timepoint height to make CSS transition smoother
'transitionend .studyTimepointThumbnails'(event, instance) {
if (event.target === event.currentTarget) {
$(event.currentTarget).closest('.studyTimepoint').trigger('displayStateChanged');
}
},
// Transfers the active state to the current study
'click .studyQuickSwitchTimepoint .studyModality'(event, instance) {
instance.select(true);
},
// Changes the current study selection for the clicked study
'click .studyModality'(event, instance) {
const $study = $(event.currentTarget).closest('.studyTimepointStudy');
const studyData = instance.data.study;
const studyInstanceUid = studyData.studyInstanceUid;
const isQuickSwitch = !_.isUndefined(instance.data.viewportIndex);
// Check if the study already has series data,
// and if not, retrieve it.
if (!studyData.seriesList) {
const alreadyLoaded = ViewerStudies.findOne({
studyInstanceUid
});
if (!alreadyLoaded) {
$study.addClass('loading');
getStudyMetadata(studyInstanceUid, study => {
study.displaySets = createStacks(study);
ViewerStudies.insert(study);
instance.select(isQuickSwitch);
});
} else {
studyData.seriesList = alreadyLoaded.seriesList;
}
} else {
instance.select(isQuickSwitch);
}
}
});
@@ -0,0 +1,131 @@
@import "{design}/app"
$boxBorderColor = transparent
$boxHoverBackgroundColor = #14191E
$boxHoverBorderColor = #2d4660
$boxActiveBorderColor = #131D29
$boxWidth = 50px
$borderThickness = 2px
$boxBorderRadius = 8px
$nestingMargin = 6px
$spacerX = 7px
$spacerY = 12px
.studyTimepointStudy
position: relative
// required transformation to make inner fixed elements relative to this one
transform(scale(1))
.loadingTextDiv
padding: 25px
border-radius: 12px
position: absolute
opacity: 0
z-index: 100
background-color: $boxHoverBackgroundColor
pointer-events: none
margin: 0
&.loading
.loadingTextDiv
height: 100%
left: 0
opacity: 0.75
right: 16px
.studyModality
opacity: 0.2
&.active
.studyModality
.studyModalityBox
theme('color', '$primaryBackgroundColor')
&
&:before
&:after
theme('background-color', '$activeColor')
border-color: $boxActiveBorderColor
&:not(.active) .studyTimepointThumbnails
max-height: 0 !important
.studyTimepointThumbnails
overflow: hidden
transition($sidebarTransition)
.studyModality
border: 1px solid $boxBorderColor
border-radius: 12px
cursor: pointer
padding: $spacerY $spacerX ($spacerY - 1)
position: relative
transition($sidebarTransition)
z-index: 1
&:hover
background-color: $boxHoverBackgroundColor
border-color: $boxHoverBorderColor
&.additional
theme('color', '$textSecondaryColor')
&.additional
theme('color', '$textPrimaryColor')
font-size: 16px
font-weight: normal
height: 91px
line-height: 91px
padding: 0
text-align: center
.studyText
font-size: 13px
left: ($spacerX * 3) + $boxWidth + ($nestingMargin * 3)
line-height: 14px
position: absolute
right: $spacerX
top: $spacerY
.studyDate
margin-top: 8px
theme('color', '$textSecondaryColor')
.studyDescription
margin-top: 8px
theme('color', '$textPrimaryColor')
.studyModalityBox
theme('color', '$textSecondaryColor')
line-height: $boxWidth
margin-left: $nestingMargin * 2
margin-top: $nestingMargin * 2
position: relative
.studyModalityText
text-align: center
text-transform: uppercase
height: 100%
&
&:before
&:after
theme('border', '$borderThickness solid $primaryBackgroundColor')
theme('background-color', '$boxBackgroundColor')
border-radius: $boxBorderRadius
height: $boxWidth + ($borderThickness * 2)
transition($sidebarTransition)
width: $boxWidth + ($borderThickness * 2)
&:before
&:after
display: block
content: ''
position: absolute
&:before
top: -($borderThickness + $nestingMargin)
left: -($borderThickness + $nestingMargin)
z-index: -1
&:after
top: -($borderThickness + ($nestingMargin * 2))
left: -($borderThickness + ($nestingMargin * 2))
z-index: -2
@@ -0,0 +1,22 @@
<template name="toolbarSectionButton">
{{#if this.buttonTemplateName}}
{{>UI.dynamic template=this.buttonTemplateName data=this}}
{{else}}
<div id="{{this.id}}"
class="toolbarSectionButton rp-x-1 {{this.classes}} {{activeClass}} {{#if this.disabled}}disabled{{/if}} {{#if this.disableFunction}}{{#if (disableButton)}}disabled{{/if}}{{/if}}"
title="{{this.title}}">
<div class="svgContainer">
{{#if this.svgLink}}
<svg>
<use xlink:href={{this.svgLink}}></use>
</svg>
{{else}}
<i class={{iconClasses}}></i>
{{/if}}
</div>
<div class="buttonLabel">
{{this.title}}
</div>
</div>
{{/if}}
</template>
@@ -0,0 +1,54 @@
import { OHIF } from 'meteor/ohif:core';
Template.toolbarSectionButton.helpers({
activeClass() {
// TODO: Find a way to prevent the 'flash' after a click, but before this helper runs
const instance = Template.instance();
// Check if the current tool is the active one
if (instance.data.id === Session.get('ToolManagerActiveTool')) {
// Return the active class
return 'active';
}
},
disableButton() {
return this.disableFunction();
}
});
Template.toolbarSectionButton.events({
'click .imageViewerTool'(event, instance) {
// Stop here if the tool is disabled
if ($(event.currentTarget).hasClass('disabled')) {
return;
}
const tool = event.currentTarget.id;
const elements = instance.$('.imageViewerViewport');
const activeTool = toolManager.getActiveTool();
if (tool === activeTool) {
const defaultTool = toolManager.getDefaultTool();
log.info('Setting active tool to: ' + defaultTool);
toolManager.setActiveTool(defaultTool, elements);
} else {
log.info('Setting active tool to: ' + tool);
toolManager.setActiveTool(tool, elements);
}
},
'click .imageViewerCommand'(event, instance) {
// Stop here if the tool is disabled
if ($(event.currentTarget).hasClass('disabled')) {
return;
}
const command = event.currentTarget.id;
if (!OHIF.viewer.functionList || !OHIF.viewer.functionList.hasOwnProperty(command)) {
return;
}
const activeViewport = Session.get('activeViewport');
const element = $('.imageViewerViewport').get(activeViewport);
OHIF.viewer.functionList[command](element);
}
});
@@ -0,0 +1,61 @@
@import "{design}/app"
.toolbarSectionButton
display: inline-block
theme('color', '$defaultColor')
theme('fill', '$defaultColor')
theme('stroke', '$defaultColor')
min-width: 30px
cursor: pointer
text-align: center
&.disabled
opacity: 0.5
cursor: not-allowed
&:hover
i,
.buttonLabel
theme('color', '$textSecondaryColor')
.buttonLabel
theme('color', '$textSecondaryColor')
font-size: 12px
font-weight: 500
.svgContainer
margin: 0 auto
text-align: center
i
theme('color', '$textSecondaryColor')
font-size: 18px
line-height: 30px
svg
background-color: transparent
margin: 2px
width: 21px
height: 21px
&:hover
.buttonLabel
theme('color', '$hoverColor')
svg
theme('fill', '$hoverColor')
theme('stroke', '$hoverColor')
i
theme('color', '$hoverColor')
&:active, &.active
.buttonLabel
theme('color', '$activeColor')
svg
theme('fill', '$activeColor')
theme('stroke', '$activeColor')
i
theme('color', '$activeColor')
@@ -0,0 +1,30 @@
<template name="toolbarSectionTools">
<div class="toolbarSectionTools pull-left m-t-1 rm-l-3">
{{#each toolbarButton in toolbarButtons}}
{{>toolbarSectionButton toolbarButton}}
{{/each}}
{{ #if extraToolbarButtons}}
<div id="moreTools" class="js-open-more-tools toolbarSectionButton rp-x-1 rm-l-3">
<div class="svgContainer">
<svg>
<use xlink:href="/packages/ohif_viewerbase/assets/icons.svg#icon-tools-more"></use>
</svg>
</div>
<div class="buttonLabel">
<span>More</span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</div>
</div>
<div class="toolbarSectionDrawerContainer">
<!-- TODO: [design] check which group of tools shall be shown -->
<div class="toolbarSectionDrawer">
{{#each toolbarButton in extraToolbarButtons}}
{{>toolbarSectionButton toolbarButton}}
{{/each}}
</div>
</div>
{{ /if }}
</div>
</template>
@@ -0,0 +1,8 @@
Template.toolbarSectionTools.events({
'click .js-open-more-tools'(event, instance) {
const $target = $(event.currentTarget);
const isActive = $target.hasClass('active');
$target.toggleClass('active', !isActive);
$target.closest('.toolbarSection').toggleClass('expanded', !isActive);
}
});
@@ -0,0 +1,34 @@
@import "{design}/app"
$distance = 12px
.toolbarSectionTools
position: relative
.toolbarSectionDrawerContainer
bottom: - ($toolbarDrawerHeight + $distance)
height: $toolbarDrawerHeight
left: 0
min-width: 100%
position: absolute
white-space: nowrap
.toolbarSectionDrawer
theme('background-color', '$uiGrayDarker')
border-radius: 7px
theme('color', '$textPrimaryColor')
content: ''
display: block
font-size: 18px
height: $toolbarDrawerHeight - $distance
text-align: center
width: 100%
#moreTools
.buttonLabel i
text-align: center
transition(all 300ms ease)
width: 8px
&.active .buttonLabel i
transform(rotateX(180deg))
@@ -0,0 +1,6 @@
<template name="viewerMain">
<div class="viewerMain">
<div id='layoutManagerTarget'>
</div>
</div>
</template>
@@ -0,0 +1,41 @@
Template.viewerMain.onCreated(() => {
// Attach the Window resize listener
// Don't use jQuery here. "window.onresize" will always be null
// If its necessary, check all the code for window.onresize getter
// and change it to jQuery._data(window, 'events')['resize'].
// Otherwise this function will be probably overrided.
// See cineDialog instance.setResizeHandler function
window.addEventListener('resize', handleResize);
// Create the synchronizer used to update reference lines
OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer('CornerstoneNewImage', cornerstoneTools.updateImageSynchronizer);
});
Template.viewerMain.onRendered(() => {
const instance = Template.instance();
instance.subscribe('hangingprotocols', () => {
const studies = instance.data.studies;
const parentElement = instance.$('#layoutManagerTarget').get(0);
window.layoutManager = new LayoutManager(parentElement, studies);
ProtocolEngine = new HP.ProtocolEngine(window.layoutManager, studies);
HP.setEngine(ProtocolEngine);
// Enable hotkeys
enableHotkeys();
});
});
Template.viewerMain.onDestroyed(() => {
log.info('viewerMain onDestroyed');
// Remove the Window resize listener
window.removeEventListener('resize', handleResize);
// Destroy the synchronizer used to update reference lines
OHIF.viewer.updateImageSynchronizer.destroy();
delete window.layoutManager;
delete ProtocolEngine;
});
@@ -0,0 +1,33 @@
@import "{design}/app"
.viewerMain
width: 100%
height: 100%
#layoutManagerTarget
width: 100%
height: 100%
transition(all 0.3s ease)
#imageViewerViewports
.viewportContainer
theme('border', '%s solid $uiBorderColorDark' % $viewportBorderThickness)
float: left
outline: 0 // Prevent blue outline in Chrome
&:hover
&.active
&:hover.active
outline: 0 // Prevent blue outline in Chrome
&:hover
theme('border', '%s solid $uiBorderColor' % $viewportBorderThickness)
&.active, &:hover.active
theme('border', '%s solid $uiBorderColorActive' % $viewportBorderThickness)
.removable
width: 100%
height: 100%
position: relative // Necessary so that the viewportOverlay is on top of the viewports
@@ -0,0 +1,8 @@
<template name="viewportOrientationMarkers">
<div class="viewportOrientationMarkers noselect">
<div class="topMid orientationMarker">
</div>
<div class="leftMid orientationMarker">
</div>
</div>
</template>
@@ -0,0 +1,58 @@
/**
* Updates the orientation labels on a Cornerstone-enabled Viewport element
* when the viewport settings change (e.g. when a horizontal flip or a rotation occurs)
*
* @param element The DOM element of the Cornerstone viewport
*/
updateOrientationMarkers = function(element) {
// Get the current viewport settings
var viewport = cornerstone.getViewport(element);
var enabledElement = cornerstone.getEnabledElement(element);
var imagePlane = cornerstoneTools.metaData.get('imagePlane', enabledElement.image.imageId);
if (!imagePlane || !imagePlane.rowCosines || !imagePlane.columnCosines) {
return;
}
var rowString = cornerstoneTools.orientation.getOrientationString(imagePlane.rowCosines);
var columnString = cornerstoneTools.orientation.getOrientationString(imagePlane.columnCosines);
var oppositeRowString = cornerstoneTools.orientation.invertOrientationString(rowString);
var oppositeColumnString = cornerstoneTools.orientation.invertOrientationString(columnString);
var markers = {
top: oppositeColumnString,
left: oppositeRowString
};
// If any vertical or horizontal flips are applied, change the orientation strings ahead of
// the rotation applications
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 topMarker = viewportOrientationMarkers.find('.topMid');
var leftMarker = viewportOrientationMarkers.find('.leftMid');
// Swap the labels accordingly if the viewport has been rotated
// This could be done in a more complex way for intermediate rotation values (e.g. 45 degrees)
if (viewport.rotation === 90 || viewport.rotation === -270) {
topMarker.text(markers.left);
leftMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.top));
} else if (viewport.rotation === -90 || viewport.rotation === 270) {
topMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.left));
leftMarker.text(markers.top);
} else if (viewport.rotation === 180 || viewport.rotation === -180) {
topMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.top));
leftMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.left));
} else {
topMarker.text(markers.top);
leftMarker.text(markers.left);
}
};
@@ -0,0 +1,18 @@
.viewportOrientationMarkers
pointer-events: none // Necessary for click-through to cornerstone element below
font-size: 15px
color: rgb(204, 204, 204)
line-height: 18px
.orientationMarker
position: absolute
.topMid
top: 5px
left: 50%
.leftMid
top: 47%
left: 5px
@@ -0,0 +1,76 @@
<template name="viewportOverlay">
<div class="imageViewerViewportOverlay noselect {{#if gt numImages 1}}controlsVisible{{/if}}">
{{#unless tagDisplaySpecified}}
<div class="topleft dicomTag">
<div>{{formatPN patientName}}</div>
<div>{{patientId}}</div>
<div class='priorIndicator'>{{prior}}</div>
</div>
<div class="topright dicomTag">
<div>{{studyDescription}}</div>
<div>{{formatDA studyDate}} {{formatTM studyTime}}</div>
</div>
<div class="bottomright dicomTag">
<div>{{#if zoom}}Zoom: {{formatNumberPrecision zoom 0}}%{{/if}}</div>
<div class="compressionIndicator">{{compression}}</div>
<div>{{wwwc}}</div>
</div>
<div class="bottomleft dicomTag">
<div>{{#if seriesNumber}}Ser: {{seriesNumber}}{{/if}}</div>
<div>{{#if gt numImages 1}}Img: {{instanceNumber}} ({{imageIndex}}/{{numImages}}){{/if}}</div>
<div>{{#if frameRate}}{{formatNumberPrecision frameRate 2}} FPS{{/if}}</div>
<div>{{imageDimensions}}</div>
<div>
<span>{{#if location}}Loc: {{formatNumberPrecision location 2}} mm{{/if}}</span>
<span>{{#if thickness}}Thick: {{formatNumberPrecision thickness 2}} mm{{/if}}</span>
<span>{{#if spacingBetweenSlices}}Spacing: {{formatNumberPrecision spacingBetweenSlices 2}} mm{{/if}}</span>
</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>{{#if seriesNumber}}Ser: {{seriesNumber}}{{/if}}</div>
<div>{{#if gt numImages 1}}Img: {{instanceNumber}} ({{imageIndex}}/{{numImages}}){{/if}}</div>
<div>{{#if frameRate}}{{formatNumberPrecision frameRate 2}} FPS{{/if}}</div>
<div>{{imageDimensions}}</div>
<div>{{seriesDescription}}</div>
<div>{{#if zoom}}Zoom: {{formatNumberPrecision zoom 0}}%{{/if}}</div>
<div class="compressionIndicator">{{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>{{#if seriesNumber}}Ser: {{seriesNumber}}{{/if}}</div>
<div>{{#if gt numImages 1}}Img: {{instanceNumber}} ({{imageIndex}}/{{numImages}}){{/if}}</div>
<div>{{#if frameRate}}{{formatNumberPrecision frameRate 2}} FPS{{/if}}</div>
<div>{{imageDimensions}}</div>
<div>{{seriesDescription}}</div>
<div>{{#if zoom}}Zoom: {{formatNumberPrecision zoom 0}}%{{/if}}</div>
<div class="compressionIndicator">{{compression}}</div>
<div>{{wwwc}}</div>
</div>
{{/if}}
{{#if gt numImages 1}}
{{>imageControls imageIndex=imageIndex numImages=numImages}}
{{/if}}
</div>
</template>
@@ -0,0 +1,337 @@
function getElementIfNotEmpty(viewportIndex) {
// Meteor template helpers run more often than expected
// They often seem to run just before the whole template is rendered
// This meant that the onRendered event hadn't fired yet, so the
// element wasn't enabled / set empty yet. The check here
// for canvases under the 'enabled' element div is to prevent
// 'undefined' errors from the helper functions
var imageViewerViewports = $('.imageViewerViewport'),
element = imageViewerViewports.get(viewportIndex),
canvases = imageViewerViewports.eq(viewportIndex).find('canvas');
if (!element || $(element).hasClass('empty') || canvases.length === 0) {
return;
}
// Check to make sure the element is enabled.
try {
var enabledElement = cornerstone.getEnabledElement(element);
} catch(error) {
return;
}
return element;
}
function getStackDataIfNotEmpty(viewportIndex) {
const element = getElementIfNotEmpty(viewportIndex);
if (!element) {
return;
}
const stackToolData = cornerstoneTools.getToolState(element, 'stack');
if (!stackToolData ||
!stackToolData.data ||
!stackToolData.data.length) {
return;
}
const stack = stackToolData.data[0];
if (!stack) {
return;
}
return stack;
}
function getPatient(property) {
Session.get('CornerstoneNewImage' + this.viewportIndex);
if (!this.imageId) {
return false;
}
var patient = cornerstoneTools.metaData.get('patient', this.imageId);
if (!patient) {
return '';
}
return patient[property];
}
function getStudy(property) {
Session.get('CornerstoneNewImage' + this.viewportIndex);
if (!this.imageId) {
return false;
}
var study = cornerstoneTools.metaData.get('study', this.imageId);
if (!study) {
return '';
}
return study[property];
}
function getSeries(property) {
Session.get('CornerstoneNewImage' + this.viewportIndex);
if (!this.imageId) {
return false;
}
var series = cornerstoneTools.metaData.get('series', this.imageId);
if (!series) {
return '';
}
return series[property];
}
function getInstance(property) {
Session.get('CornerstoneNewImage' + this.viewportIndex);
if (!this.imageId) {
return false;
}
var instance = cornerstoneTools.metaData.get('instance', this.imageId);
if (!instance) {
return '';
}
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) {
return false;
}
var enabledElement;
try {
enabledElement = cornerstone.getEnabledElement(element);
} catch(error) {
return false;
}
if (!enabledElement || !enabledElement.image) {
return false;
}
return enabledElement.image;
}
function formatDateTime(date, time) {
return `${date} ${time}`;
}
Template.viewportOverlay.helpers({
wwwc: function() {
Session.get('CornerstoneImageRendered' + this.viewportIndex);
var element = getElementIfNotEmpty(this.viewportIndex);
if (!element) {
return '';
}
var viewport = cornerstone.getViewport(element);
if (!viewport) {
return '';
}
return 'W ' + viewport.voi.windowWidth.toFixed(0) + ' L ' + viewport.voi.windowCenter.toFixed(0);
},
zoom: function() {
Session.get('CornerstoneImageRendered' + this.viewportIndex);
var element = getElementIfNotEmpty(this.viewportIndex);
if (!element) {
return;
}
var viewport = cornerstone.getViewport(element);
if (!viewport) {
return;
}
return (viewport.scale * 100.0);
},
imageDimensions: function() {
Session.get('CornerstoneNewImage' + this.viewportIndex);
var image = getImage(this.viewportIndex);
if (!image) {
return '';
}
return image.width + ' x ' + image.height;
},
patientName: function() {
return getPatient.call(this, 'name');
},
patientId: function() {
return getPatient.call(this, 'id');
},
patientBirthDate: function() {
return getPatient.call(this, 'birthDate');
},
patientSex: function() {
return getPatient.call(this, 'sex');
},
studyDate: function() {
return getStudy.call(this, 'studyDate');
},
studyTime: function() {
return getStudy.call(this, 'studyTime');
},
studyDescription: function() {
return getStudy.call(this, 'studyDescription');
},
seriesDescription: function() {
return getSeries.call(this, 'seriesDescription');
},
frameRate: function() {
var frameTime = getInstance.call(this, 'frameTime');
if (!frameTime) {
return;
}
var frameRate = 1000 / frameTime;
return frameRate.toFixed(1);
},
seriesNumber: function() {
return getSeries.call(this, 'seriesNumber');
},
instanceNumber: function() {
return getInstance.call(this, 'instanceNumber');
},
thickness() {
// Displays Slice Thickness (0018,0050)
return getInstance.call(this, 'sliceThickness');
},
location() {
// Displays Slice Location (0020,1041), if present.
// - Otherwise, displays Table Position (0018,9327)
// - TODO: Otherwise, displays a value derived from Image Position (Patient) (0020,0032)
const sliceLocation = getInstance.call(this, 'sliceLocation');
if (sliceLocation !== '') {
return sliceLocation;
}
const tablePosition = getInstance.call(this, 'tablePosition');
if (tablePosition !== '') {
return tablePosition;
}
return getInstance.call(this, 'imagePositionPatient');
},
spacingBetweenSlices() {
// Displays Spacing Between Slices (0018,0088), if present.
// TODO: Otherwise, displays a value derived from successive values
// of Image Position (Patient) (0020,0032) perpendicular to
// the Image Orientation (Patient) (0020,0037)
return getInstance.call(this, 'spacingBetweenSlices');
},
compression() {
// Displays whether or not lossy compression has been applied:
//
// - Checks Lossy Image Compression (0028,2110)
// - If so, displays the value of Lossy Image Compression Ratio (0028,2112)
// and Lossy Image Compression Method (0028,2114)
Session.get('CornerstoneNewImage' + this.viewportIndex);
if (!this.imageId) {
return false;
}
var instance = cornerstoneTools.metaData.get('instance', this.imageId);
if (!instance) {
return '';
}
if (instance.lossyImageCompression === '01' &&
instance.lossyImageCompressionRatio !== '') {
const compressionMethod = instance.lossyImageCompressionMethod || 'Lossy: ';
const compressionRatio = parseFloat(instance.lossyImageCompressionRatio).toFixed(2);
return compressionMethod + compressionRatio + ' : 1';
}
return 'Lossless / Uncompressed';
},
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() {
Session.get('CornerstoneNewImage' + this.viewportIndex);
var stack = getStackDataIfNotEmpty(this.viewportIndex);
if (!stack || stack.currentImageIdIndex === undefined) {
return;
}
return stack.currentImageIdIndex + 1;
},
numImages: function() {
Session.get('CornerstoneNewImage' + this.viewportIndex);
var stack = getStackDataIfNotEmpty(this.viewportIndex);
if (!stack || !stack.imageIds) {
return;
}
return stack.imageIds.length;
},
prior: function() {
// This helper is updated whenever a new image is displayed in the viewport
Session.get('CornerstoneNewImage' + this.viewportIndex);
if (!this.imageId) {
return;
}
// Make sure there are more than two studies loaded in the viewer
var viewportStudies = ViewerStudies.find();
if (viewportStudies.count() < 2) {
return;
}
// Here we sort the collection in ascending order by study date, so
// that we can obtain the oldest study as the first element of the array
//
// TODO= Find out if we should encode studyDate as a Date in the ViewerStudies Collection
var viewportStudiesArray = _.sortBy(viewportStudies.fetch(), function(study) {
return formatDateTime(study.studyDate, study.studyTime);
});
// Get study data
var study = cornerstoneTools.metaData.get('study', this.imageId);
if (!study) {
return;
}
var oldestStudy = viewportStudiesArray[0];
if (formatDateTime(study.studyDate, study.studyTime) <= formatDateTime(oldestStudy.studyDate, oldestStudy.studyTime)) {
return 'Prior';
}
}
});
@@ -0,0 +1,41 @@
@import "{design}/app"
$viewportTagPadding = 20px
.imageViewerViewport.empty ~ .imageViewerViewportOverlay
display: none
.imageViewerViewportOverlay
theme('color', '$textSecondaryColor')
.dicomTag
position: absolute
font-weight: 400
text-shadow: 1px 1px black
pointer-events: none
.topleft
top: $viewportTagPadding
left: $viewportTagPadding
.topright
top: $viewportTagPadding
right: $viewportTagPadding
text-align: right
.bottomleft
bottom: $viewportTagPadding
left: $viewportTagPadding
.bottomright
bottom: $viewportTagPadding
right: $viewportTagPadding
text-align: right
.priorIndicator
font-weight: bold
color: yellow
&.controlsVisible
.topright, .bottomright
right: "calc(%s + 19px)" % $viewportTagPadding