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:
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">×</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
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
<template name="removableBackdrop">
|
||||
<div class="removableBackdrop noselect"
|
||||
oncontextmenu='return false;'
|
||||
unselectable='on'
|
||||
onselectstart='return false;'>
|
||||
</div>
|
||||
</template>
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
.removableBackdrop
|
||||
position: fixed
|
||||
top: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
z-index: 50
|
||||
background-color: transparent
|
||||
|
||||
+11
@@ -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>
|
||||
+87
@@ -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 + '%';
|
||||
}
|
||||
}
|
||||
});
|
||||
+49
@@ -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
|
||||
+11
@@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
+19
@@ -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
|
||||
+30
@@ -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>
|
||||
+279
@@ -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;
|
||||
}
|
||||
});
|
||||
+98
@@ -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
|
||||
+33
@@ -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>
|
||||
+126
@@ -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));
|
||||
});
|
||||
});
|
||||
|
||||
+30
@@ -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
|
||||
+16
@@ -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>
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
function closeHandler() {
|
||||
// Hide the lesion dialog
|
||||
$('#confirmDeleteDialog').css('display', 'none');
|
||||
|
||||
// Remove the backdrop
|
||||
$('.removableBackdrop').remove();
|
||||
|
||||
// Remove the callback from the template data
|
||||
delete Template.confirmDeleteDialog.doneCallback;
|
||||
|
||||
// Restore the focus to the active viewport
|
||||
setFocusToActiveViewport();
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the confirmation dialog template and the removable backdrop element
|
||||
*
|
||||
* @param doneCallback A callback
|
||||
* @param options
|
||||
*/
|
||||
showConfirmDialog = function(doneCallback, options) {
|
||||
// Show the backdrop
|
||||
options = options || {};
|
||||
UI.renderWithData(Template.removableBackdrop, options, document.body);
|
||||
|
||||
var confirmDeleteDialog = $('#confirmDeleteDialog');
|
||||
confirmDeleteDialog.remove();
|
||||
|
||||
var viewer = document.getElementById('viewer');
|
||||
UI.renderWithData(Template.confirmDeleteDialog, options, viewer);
|
||||
|
||||
// Make sure the context menu is closed when the user clicks away
|
||||
$('.removableBackdrop').one('mousedown touchstart', function() {
|
||||
closeHandler();
|
||||
});
|
||||
|
||||
confirmDeleteDialog = $('#confirmDeleteDialog');
|
||||
confirmDeleteDialog.css('display', 'block');
|
||||
confirmDeleteDialog.focus();
|
||||
|
||||
if (doneCallback && typeof doneCallback === 'function') {
|
||||
Template.confirmDeleteDialog.doneCallback = doneCallback;
|
||||
}
|
||||
};
|
||||
|
||||
// Global object of key names (TODO: put this somewhere else)
|
||||
keys = {
|
||||
ESC: 27,
|
||||
ENTER: 13
|
||||
};
|
||||
|
||||
Template.confirmDeleteDialog.events({
|
||||
'click #cancel, click #close': function() {
|
||||
closeHandler();
|
||||
},
|
||||
'click #confirm': function() {
|
||||
var doneCallback = Template.confirmDeleteDialog.doneCallback;
|
||||
if (doneCallback && typeof doneCallback === 'function') {
|
||||
doneCallback();
|
||||
}
|
||||
|
||||
closeHandler();
|
||||
},
|
||||
'keydown #confirmDeleteDialog': function(e) {
|
||||
if (e.which === keys.ESC) {
|
||||
closeHandler();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.keyPressAllowed === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
var doneCallback = Template.confirmDeleteDialog.doneCallback;
|
||||
|
||||
// If Enter is pressed, close the dialog
|
||||
if (e.which === keys.ENTER) {
|
||||
if (doneCallback && typeof doneCallback === 'function') {
|
||||
doneCallback();
|
||||
}
|
||||
|
||||
closeHandler();
|
||||
}
|
||||
}
|
||||
});
|
||||
+25
@@ -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
|
||||
+16
@@ -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>
|
||||
+27
@@ -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
|
||||
+14
@@ -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>
|
||||
+550
@@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
+37
@@ -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')
|
||||
+14
@@ -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>
|
||||
+72
@@ -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 + '%';
|
||||
}
|
||||
}
|
||||
});
|
||||
+39
@@ -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();
|
||||
}
|
||||
});
|
||||
+42
@@ -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>
|
||||
+45
@@ -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();
|
||||
}
|
||||
});
|
||||
+249
@@ -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
|
||||
+12
@@ -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>
|
||||
+90
@@ -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];
|
||||
}
|
||||
});
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
@import "{design}/app"
|
||||
|
||||
.studyTimepointWrapper
|
||||
overflow: hidden
|
||||
transition($sidebarTransition)
|
||||
|
||||
.timepointEntry:not(.active) .studyTimepointWrapper
|
||||
max-height: 0 !important
|
||||
+42
@@ -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>
|
||||
+165
@@ -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(', ');
|
||||
}
|
||||
});
|
||||
+104
@@ -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
|
||||
+31
@@ -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>
|
||||
+113
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
+131
@@ -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
|
||||
+22
@@ -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>
|
||||
+54
@@ -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);
|
||||
}
|
||||
});
|
||||
+61
@@ -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')
|
||||
+30
@@ -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>
|
||||
+8
@@ -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);
|
||||
}
|
||||
});
|
||||
+34
@@ -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
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<template name="viewportOrientationMarkers">
|
||||
<div class="viewportOrientationMarkers noselect">
|
||||
<div class="topMid orientationMarker">
|
||||
</div>
|
||||
<div class="leftMid orientationMarker">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+58
@@ -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);
|
||||
}
|
||||
};
|
||||
+18
@@ -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
|
||||
|
||||
+76
@@ -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';
|
||||
}
|
||||
}
|
||||
});
|
||||
+41
@@ -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
|
||||
Reference in new issue
Block a user