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
@@ -1,198 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<symbol id="icon-hud" viewBox="0 0 20 22">
|
||||
<title>
|
||||
HUD
|
||||
</title>
|
||||
<g id="icon-hud-group" fill="none" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path id="icon-hud-dashes" d="m14.5,2.75 2,0 a2.5 2.5 0 0 1 2.5 2.5 l0,13.25 a2.5 2.5 0 0 1 -2.5 2.5 l-11.5,0 a2.5 2.5 0 0 1 -2.5 -2.5 l0,-1" stroke-dasharray="1,3" />
|
||||
<path id="icon-hud-box" d="m0.5,2 0,14 a1.5 1.5 0 0 0 1.5 1.5 l12,0 a1.5 1.5 0 0 0 1.5 -1.5 l0,-14" />
|
||||
<path id="icon-hud-top" d="m2,2 12,0" stroke-width="4" />
|
||||
<path id="icon-hud-paralel" d="m4.4,7.5 7.2,0 m0,3 -7.2,0 m0,3 7.2,0" stroke-linecap="square" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-measurements-additional" viewBox="0 0 16 16">
|
||||
<title>
|
||||
Additional Measurements
|
||||
</title>
|
||||
<g id="icon-measurements-additional-group" stroke="none">
|
||||
<path id="icon-measurements-additional-boxes" d="M11,2 l1.85,-1.85 a1 1 0 0 1 1,0 l2,2 a1 1 0 0 1 0,1 l-1.85,1.85Z M10,3 l-10,10 0,3 3,0 10,-10Z M16,16 l0,-2 -9,0 -2,2Z" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-measurements-lesions" viewBox="0 0 18 10">
|
||||
<title>
|
||||
Lesions
|
||||
</title>
|
||||
<g id="icon-measurements-lesions-group" stroke="none">
|
||||
<path id="icon-measurements-lesions-boxes" d="M0,0 2,0 2,2 0,2Z M4,0 18,0 18,2 4,2Z M0,4 2,4 2,6 0,6Z M4,4 18,4 18,6 4,6Z M0,8 2,8 2,10 0,10Z M4,8 18,8 18,10 4,10Z" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-settings" viewBox="0 0 15 15">
|
||||
<title>
|
||||
Settings
|
||||
</title>
|
||||
<path d="M12.85 8.25C12.85 8.03 12.93 7.8 12.93 7.5 12.93 7.2 12.93 6.98 12.85 6.75L14.43 5.48C14.58 5.33 14.58 5.18 14.5 5.03L13 2.4C12.93 2.33 12.78 2.25 12.55 2.33L10.68 3.08C10.3 2.78 9.85 2.55 9.4 2.33L9.1 0.38C9.18 0.15 8.95 0 8.8 0L5.8 0C5.65 0 5.43 0.15 5.43 0.3L5.13 2.33C4.68 2.48 4.3 2.78 3.85 3.08L2.05 2.33C1.83 2.25 1.68 2.33 1.53 2.48L0.03 5.1C-0.05 5.18 0.03 5.4 0.18 5.55L1.75 6.75C1.75 6.98 1.68 7.2 1.68 7.5 1.68 7.8 1.68 8.03 1.75 8.25L0.18 9.53C0.03 9.68 0.03 9.82 0.1 9.98L1.6 12.6C1.68 12.68 1.83 12.75 2.05 12.68L3.93 11.93C4.3 12.23 4.75 12.45 5.2 12.68L5.5 14.63C5.5 14.78 5.65 14.93 5.88 14.93L8.88 14.93C9.03 14.93 9.25 14.78 9.25 14.63L9.55 12.68C10 12.45 10.45 12.23 10.83 11.93L12.7 12.68C12.85 12.75 13.08 12.68 13.15 12.53L14.65 9.9C14.73 9.75 14.73 9.53 14.58 9.45L12.85 8.25 12.85 8.25ZM7.3 10.13C5.88 10.13 4.68 8.93 4.68 7.5 4.68 6.08 5.88 4.88 7.3 4.88 8.73 4.88 9.93 6.08 9.93 7.5 9.93 8.93 8.73 10.13 7.3 10.13L7.3 10.13Z" id="Shape"/>
|
||||
</symbol>
|
||||
<symbol id="icon-status-complete" viewBox="0 0 27 27">
|
||||
<title>
|
||||
Complete
|
||||
</title>
|
||||
<g id="icon-status-complete-group" stroke="none">
|
||||
<path id="icon-status-complete-circle" d="M13.5,0 a13.5 13.5 0 0 1 0 27 a13.5 13.5 0 0 1 0 -27 M23,8 l-2,-2 -10,10, -5,-5 -2,2 7,7Z" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-status-lock" viewBox="0 0 16 21">
|
||||
<title>
|
||||
Locked
|
||||
</title>
|
||||
<path d="M14 7L13 7 13 5C13 2.2 10.8 0 8 0 5.2 0 3 2.2 3 5L3 7 2 7C0.9 7 0 7.9 0 9L0 19C0 20.1 0.9 21 2 21L14 21C15.1 21 16 20.1 16 19L16 9C16 7.9 15.1 7 14 7L14 7ZM11.1 7L4.9 7 4.9 5C4.9 3.3 6.3 1.9 8 1.9 9.7 1.9 11.1 3.3 11.1 5L11.1 7 11.1 7Z"/>
|
||||
</symbol>
|
||||
<symbol id="icon-studies" viewBox="0 0 15 13">
|
||||
<title>
|
||||
Studies
|
||||
</title>
|
||||
<g id="icon-studies-group" stroke="none">
|
||||
<path id="icon-studies-boxes" d="M0,0 7,0 7,6 0,6Z M8,0 15,0 15,6 8,6Z M0,7 7,7 7,13 0,13Z M8,7 15,7 15,13 8,13Z" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-levels" viewBox="0 0 18 18">
|
||||
<title>
|
||||
Window / Level
|
||||
</title>
|
||||
<g id="icon-tools-levels-group">
|
||||
<path id="icon-tools-levels-path" d="M14.5,3.5 a1 1 0 0 1 -11,11 Z" stroke="none" opacity="0.8" />
|
||||
<circle id="icon-tools-levels-circle" cx="9" cy="9" r="8" fill="none" stroke-width="2" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-link" viewBox="0 0 23 15">
|
||||
<title>
|
||||
Link
|
||||
</title>
|
||||
<g id="icon-tools-link-group" stroke="none">
|
||||
<circle id="icon-tools-link-circle-l" opacity="0.8" cx="7.5" cy="7.5" r="7.5" />
|
||||
<circle id="icon-tools-link-circle-r" opacity="0.6" cx="15.5" cy="7.5" r="7.5" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-measure-non-target" viewBox="0 0 24 24">
|
||||
<title>
|
||||
Non-Target Measurement
|
||||
</title>
|
||||
<g id="icon-tools-measure-non-target-group" fill="none" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle id="icon-tools-measure-non-target-circle" cx="6.5" cy="6.5" r="6" />
|
||||
<path id="icon-tools-measure-non-target-plus" d="M6.5,3 l0,7 M3,6.5 l7,0"></path>
|
||||
<path id="icon-tools-measure-non-target-arrow" d="M23,7 l-15,15 M7,17 l0,6 6,0" stroke-width="2" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-measure-target" viewBox="0 0 26 26">
|
||||
<title>
|
||||
Target Measurement
|
||||
</title>
|
||||
<g id="icon-tools-measure-target-group" stroke-width="0">
|
||||
<path id="icon-tools-measure-target-ruler" d="m9.5,26 2,-2 -1.5,-1.5 0.5,-0.5 1.5,1.5 1.5,-1.5 -2.5,-2.5 0.5,-0.5 2.5,2.5 1.5,-1.5 -1.5,-1.5 0.5,-0.5 1.5,1.5 1.5,-1.5 -2.5,-2.5 0.5,-0.5 2.5,2.5 1.5,-1.5 -1.5,-1.5 0.5,-0.5 1.5,1.5 1.5,-1.5 -2.5,-2.5 0.5,-0.5 2.5,2.5 1.5,-1.5 -1.5,-1.5 0.5,-0.5 1.5,1.5 2,-2 -4,-4 a2 2 0 0 0 -2.5 0 l-14 14 a2 2 0 0 0 0 2.5 l4,4 Z" />
|
||||
<path id="icon-tools-measure-target-plus" d="m6,0 a6 6 0 0 1 0 12 a6 6 0 0 1 0 -12 m0.5,3 a0.5 0.5 0 0 0 -1 0 l0,2.5 -2.5,0 a0.5 0.5 0 0 0 0 1 l2.5,0 0,2.5 a0.5 0.5 0 0 0 1 0 l0,-2.5 2.5,0 a0.5 0.5 0 0 0 0 -1 l-2.5,0 z" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-measure-temp" viewBox="0 0 24 24">
|
||||
<title>
|
||||
Temporary Measurement
|
||||
</title>
|
||||
<g id="icon-tools-measure-temp-group" fill="none" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle id="icon-tools-measure-temp-circle" cx="6.5" cy="6.5" r="6" />
|
||||
<path id="icon-tools-measure-temp-plus" d="M6.5,3 l0,7 M3,6.5 l7,0"></path>
|
||||
<path id="icon-tools-measure-temp-dashes" d="m22.5,6 -16.5,16.5" stroke-width="3" stroke-dasharray="0.6666,5" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-more" viewBox="0 0 18 18">
|
||||
<title>
|
||||
More
|
||||
</title>
|
||||
<g id="icon-tools-more-group" stroke="none">
|
||||
<path id="icon-tools-more-circle" d="M9,0 a9 9 0 0 1 0 18 a9 9 0 0 1 0 -18 M4.5,8 a1.5 1.5 0 0 0 0 3 a1.5 1.5 0 0 0 0 -3 M9,8 a1.5 1.5 0 0 0 0 3 a1.5 1.5 0 0 0 0 -3 M13.5,8 a1.5 1.5 0 0 0 0 3 a1.5 1.5 0 0 0 0 -3" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-pan" viewBox="0 0 18 18">
|
||||
<title>
|
||||
Pan
|
||||
</title>
|
||||
<g id="icon-tools-pan-group" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path id="icon-tools-pan-line-v" d="M9,1 L9,17"></path>
|
||||
<path id="icon-tools-pan-line-h" d="M1,9 L17,9"></path>
|
||||
<polyline id="icon-tools-pan-caret-t" points="7 3 9 1 11 3"></polyline>
|
||||
<polyline id="icon-tools-pan-caret-r" points="15 11 17 9 15 7"></polyline>
|
||||
<polyline id="icon-tools-pan-caret-b" points="11 15 9 17 7 15"></polyline>
|
||||
<polyline id="icon-tools-pan-caret-l" points="3 7 1 9 3 11"></polyline>
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-zoom" viewBox="0 0 17 17">
|
||||
<title>
|
||||
Zoom
|
||||
</title>
|
||||
<g id="icon-tools-zoom-group" fill="none" stroke-width="2" stroke-linecap="round">
|
||||
<path id="icon-tools-zoom-path" d="m11.5,11.5 4.5,4.5" />
|
||||
<circle id="icon-tools-zoom-circle" cx="7" cy="7" r="6" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-trial-info" viewBox="0 0 20 20">
|
||||
<title>
|
||||
Trial Information
|
||||
</title>
|
||||
<g id="icon-trial-info-group" fill="none" stroke-width="2">
|
||||
<path id="icon-trial-info-circle" d="M10,1 a9 9 0 0 1 0 18 a9 9 0 0 1 0 -18 M10,5 l0,2 m0,2 0,6" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-ui-accordion-expand" viewBox="0 0 12 8">
|
||||
<title>
|
||||
Expand
|
||||
</title>
|
||||
<polygon transform="translate(6.000000, 3.700000) rotate(-270.000000) translate(-6.000000, -3.700000) " points="3.7 -2.3 2.3 -0.9 6.9 3.7 2.3 8.3 3.7 9.7 9.7 3.7"/>
|
||||
</symbol>
|
||||
<symbol id="icon-ui-add" viewBox="0 0 12 13">
|
||||
<title>
|
||||
Add
|
||||
</title>
|
||||
<path d="M6.5 7L6.5 12C6.5 12.28 6.28 12.5 6 12.5 5.72 12.5 5.5 12.28 5.5 12L5.5 7 0.5 7C0.22 7 0 6.78 0 6.5 0 6.22 0.22 6 0.5 6L5.5 6 5.5 1C5.5 0.72 5.72 0.5 6 0.5 6.28 0.5 6.5 0.72 6.5 1L6.5 6 11.5 6C11.78 6 12 6.22 12 6.5 12 6.78 11.78 7 11.5 7L6.5 7Z" id="Combined-Shape"/>
|
||||
</symbol>
|
||||
<symbol id="icon-ui-close" viewBox="0 0 14 14">
|
||||
<title>
|
||||
Close
|
||||
</title>
|
||||
<g id="icon-ui-close-group" stroke-width="1.75">
|
||||
<path id="icon-ui-close-path" d="M1,1 13,13 M1,13 13,1" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-create-comment" viewBox="0 0 37 34">
|
||||
<title>
|
||||
Comment
|
||||
</title>
|
||||
<path fill="#94a8b3" d="M22.32 14L35 14C36.1 14 37 14.9 37 16L37 28C37 29.1 36.1 30 35 30L21 30 17 34 17 20.39C19.45 18.97 21.36 16.71 22.32 14Z" id="Combined-Shape"/>
|
||||
<path d="M10 0C4.5 0 0 4.5 0 10 0 15.5 4.5 20 10 20 15.5 20 20 15.5 20 10 20 4.5 15.5 0 10 0L10 0ZM15 11L11 11 11 15 9 15 9 11 5 11 5 9 9 9 9 5 11 5 11 9 15 9 15 11 15 11Z" id="Shape"/>
|
||||
</symbol>
|
||||
<symbol id="icon-create-screen-capture" viewBox="0 0 37 35">
|
||||
<title>
|
||||
Capture Screen
|
||||
</title>
|
||||
<g id="camera-alt" transform="translate(17.000000, 17.000000)" fill="#94a8b3">
|
||||
<path d="M10 13C11.66 13 13 11.66 13 10 13 8.34 11.66 7 10 7 8.34 7 7 8.34 7 10 7 11.66 8.34 13 10 13Z" id="Oval"/>
|
||||
<path d="M7 0L5.2 2 2 2C0.9 2 0 2.9 0 4L0 16C0 17.1 0.9 18 2 18L18 18C19.1 18 20 17.1 20 16L20 4C20 2.9 19.1 2 18 2L14.8 2 13 0 7 0 7 0ZM10 15C7.2 15 5 12.8 5 10 5 7.2 7.2 5 10 5 12.8 5 15 7.2 15 10 15 12.8 12.8 15 10 15L10 15Z" id="Shape"/>
|
||||
</g>
|
||||
<path d="M10 0C4.5 0 0 4.5 0 10 0 15.5 4.5 20 10 20 15.5 20 20 15.5 20 10 20 4.5 15.5 0 10 0L10 0ZM15 11L11 11 11 15 9 15 9 11 5 11 5 9 9 9 9 5 11 5 11 9 15 9 15 11 15 11Z" id="Shape"/>
|
||||
</symbol>
|
||||
<symbol id="icon-ui-warning" viewBox="0 0 22 20">
|
||||
<title>
|
||||
Warning
|
||||
</title>
|
||||
<g id="icon-ui-warning-group" stroke="none">
|
||||
<path d="m11,0 11,20 -22,0Z m1,7 -2,0 0,6 2,0Z m0,8 -2,0 0,2 2,0Z" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-ohif-logo" viewBox="0 0 47 47">
|
||||
<title>
|
||||
Open Health Imaging Foundation
|
||||
</title>
|
||||
<g id="icon-ohif-logo" stroke="#fff" stroke-width="2.5" stroke-miterlimit="10">
|
||||
<rect x="1.25" y="1.25" width="18.8" height="18.8" rx="1" ry="1"/>
|
||||
<rect x="26" y="1.25" width="18.8" height="18.8" rx="1" ry="1"/>
|
||||
<rect x="1.25" y="26" width="18.8" height="18.8" rx="1" ry="1"/>
|
||||
<rect x="26" y="26" width="18.8" height="18.8" rx="1" ry="1"/>
|
||||
</g>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 10 KiB |
@@ -1,6 +0,0 @@
|
||||
ViewerStudies = new Meteor.Collection(null);
|
||||
ViewerStudies._debugName = 'ViewerStudies';
|
||||
|
||||
ClientId = Random.id();
|
||||
|
||||
Meteor.subscribe('studyImportStatus');
|
||||
@@ -1,412 +0,0 @@
|
||||
var dialogPolyfill = (function() {
|
||||
|
||||
var supportCustomEvent = window.CustomEvent;
|
||||
if (!supportCustomEvent || typeof supportCustomEvent == "object") {
|
||||
supportCustomEvent = function CustomEvent(event, x) {
|
||||
x = x || {};
|
||||
var ev = document.createEvent('CustomEvent');
|
||||
ev.initCustomEvent(event, !!x.bubbles, !!x.cancelable, x.detail || null);
|
||||
return ev;
|
||||
};
|
||||
supportCustomEvent.prototype = window.Event.prototype;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the nearest <dialog> from the passed element.
|
||||
*
|
||||
* @param {Element} el to search from
|
||||
* @param {HTMLDialogElement} dialog found
|
||||
*/
|
||||
function findNearestDialog(el) {
|
||||
while (el) {
|
||||
if (el.nodeName == 'DIALOG') {
|
||||
return el;
|
||||
}
|
||||
el = el.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var dialogPolyfill = {};
|
||||
|
||||
dialogPolyfill.reposition = function(element) {
|
||||
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
|
||||
var topValue = scrollTop + (window.innerHeight - element.offsetHeight) / 2;
|
||||
element.style.top = Math.max(0, topValue) + 'px';
|
||||
element.dialogPolyfillInfo.isTopOverridden = true;
|
||||
};
|
||||
|
||||
dialogPolyfill.inNodeList = function(nodeList, node) {
|
||||
for (var i = 0; i < nodeList.length; ++i) {
|
||||
if (nodeList[i] == node)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
dialogPolyfill.isInlinePositionSetByStylesheet = function(element) {
|
||||
for (var i = 0; i < document.styleSheets.length; ++i) {
|
||||
var styleSheet = document.styleSheets[i];
|
||||
var cssRules = null;
|
||||
// Some browsers throw on cssRules.
|
||||
try {
|
||||
cssRules = styleSheet.cssRules;
|
||||
} catch (e) {}
|
||||
if (!cssRules)
|
||||
continue;
|
||||
for (var j = 0; j < cssRules.length; ++j) {
|
||||
var rule = cssRules[j];
|
||||
var selectedNodes = null;
|
||||
// Ignore errors on invalid selector texts.
|
||||
try {
|
||||
selectedNodes = document.querySelectorAll(rule.selectorText);
|
||||
} catch(e) {}
|
||||
if (!selectedNodes || !dialogPolyfill.inNodeList(selectedNodes, element))
|
||||
continue;
|
||||
var cssTop = rule.style.getPropertyValue('top');
|
||||
var cssBottom = rule.style.getPropertyValue('bottom');
|
||||
if ((cssTop && cssTop != 'auto') || (cssBottom && cssBottom != 'auto'))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
dialogPolyfill.needsCentering = function(dialog) {
|
||||
var computedStyle = window.getComputedStyle(dialog);
|
||||
if (computedStyle.position != 'absolute') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We must determine whether the top/bottom specified value is non-auto. In
|
||||
// WebKit/Blink, checking computedStyle.top == 'auto' is sufficient, but
|
||||
// Firefox returns the used value. So we do this crazy thing instead: check
|
||||
// the inline style and then go through CSS rules.
|
||||
if ((dialog.style.top != 'auto' && dialog.style.top != '') ||
|
||||
(dialog.style.bottom != 'auto' && dialog.style.bottom != ''))
|
||||
return false;
|
||||
return !dialogPolyfill.isInlinePositionSetByStylesheet(dialog);
|
||||
};
|
||||
|
||||
dialogPolyfill.showDialog = function(isModal) {
|
||||
if (this.open) {
|
||||
throw 'InvalidStateError: showDialog called on open dialog';
|
||||
}
|
||||
this.open = true; // TODO: should be a getter mapped to attribute
|
||||
this.setAttribute('open', 'open');
|
||||
|
||||
if (isModal) {
|
||||
// Find element with `autofocus` attribute or first form control
|
||||
var first_form_ctrl = null;
|
||||
var autofocus = null;
|
||||
var findElementToFocus = function(root) {
|
||||
if (!root.children) {
|
||||
return;
|
||||
}
|
||||
for (var i = 0; i < root.children.length; i++) {
|
||||
var elem = root.children[i];
|
||||
if (first_form_ctrl === null && !elem.disabled && (
|
||||
elem.nodeName == 'BUTTON' ||
|
||||
elem.nodeName == 'INPUT' ||
|
||||
elem.nodeName == 'KEYGEN' ||
|
||||
elem.nodeName == 'SELECT' ||
|
||||
elem.nodeName == 'TEXTAREA')) {
|
||||
first_form_ctrl = elem;
|
||||
}
|
||||
if (elem.autofocus) {
|
||||
autofocus = elem;
|
||||
return;
|
||||
}
|
||||
findElementToFocus(elem);
|
||||
if (autofocus !== null) return;
|
||||
}
|
||||
};
|
||||
|
||||
findElementToFocus(this);
|
||||
|
||||
if (autofocus !== null) {
|
||||
autofocus.focus();
|
||||
} else if (first_form_ctrl !== null) {
|
||||
first_form_ctrl.focus();
|
||||
}
|
||||
}
|
||||
|
||||
if (dialogPolyfill.needsCentering(this))
|
||||
dialogPolyfill.reposition(this);
|
||||
if (isModal) {
|
||||
this.dialogPolyfillInfo.modal = true;
|
||||
dialogPolyfill.dm.pushDialog(this);
|
||||
}
|
||||
|
||||
// IE sometimes complains when calling .focus() that it
|
||||
// "Can't move focus to the control because it is invisible, not enabled, or of a type that does not accept the focus."
|
||||
try {
|
||||
if (autofocus !== null) {
|
||||
autofocus.focus();
|
||||
} else if (first_form_ctrl !== null) {
|
||||
first_form_ctrl.focus();
|
||||
}
|
||||
} catch(e) {}
|
||||
this.style.zoom = 1;
|
||||
};
|
||||
|
||||
dialogPolyfill.close = function(retval) {
|
||||
if (!this.open && !window.HTMLDialogElement) {
|
||||
// Native implementations will set .open to false, so ignore this error.
|
||||
throw 'InvalidStateError: close called on closed dialog';
|
||||
}
|
||||
this.open = false;
|
||||
this.removeAttribute('open');
|
||||
|
||||
// Leave returnValue untouched in case it was set directly on the element
|
||||
if (typeof retval != 'undefined') {
|
||||
this.returnValue = retval;
|
||||
}
|
||||
|
||||
// This won't match the native <dialog> exactly because if the user sets top
|
||||
// on a centered polyfill dialog, that top gets thrown away when the dialog is
|
||||
// closed. Not sure it's possible to polyfill this perfectly.
|
||||
if (this.dialogPolyfillInfo.isTopOverridden) {
|
||||
this.style.top = 'auto';
|
||||
}
|
||||
|
||||
if (this.dialogPolyfillInfo.modal) {
|
||||
dialogPolyfill.dm.removeDialog(this);
|
||||
}
|
||||
|
||||
// Triggering "close" event for any attached listeners on the <dialog>
|
||||
var event;
|
||||
if (document.createEvent) {
|
||||
event = document.createEvent('HTMLEvents');
|
||||
event.initEvent('close', true, true);
|
||||
} else {
|
||||
event = new Event('close');
|
||||
}
|
||||
this.dispatchEvent(event);
|
||||
|
||||
return this.returnValue;
|
||||
};
|
||||
|
||||
dialogPolyfill.registerDialog = function(element) {
|
||||
if (element.show) {
|
||||
console.warn("This browser already supports <dialog>, the polyfill " +
|
||||
"may not work correctly.");
|
||||
}
|
||||
element.show = dialogPolyfill.showDialog.bind(element, false);
|
||||
element.showModal = dialogPolyfill.showDialog.bind(element, true);
|
||||
element.close = dialogPolyfill.close.bind(element);
|
||||
element.dialogPolyfillInfo = {};
|
||||
element.open = false;
|
||||
};
|
||||
|
||||
// The overlay is used to simulate how a modal dialog blocks the document. The
|
||||
// blocking dialog is positioned on top of the overlay, and the rest of the
|
||||
// dialogs on the pending dialog stack are positioned below it. In the actual
|
||||
// implementation, the modal dialog stacking is controlled by the top layer,
|
||||
// where z-index has no effect.
|
||||
var TOP_LAYER_ZINDEX = 100000;
|
||||
var MAX_PENDING_DIALOGS = 100000;
|
||||
|
||||
dialogPolyfill.DialogManager = function() {
|
||||
this.pendingDialogStack = [];
|
||||
this.overlay = document.createElement('div');
|
||||
this.overlay.style.width = '100%';
|
||||
this.overlay.style.height = '100%';
|
||||
this.overlay.style.position = 'fixed';
|
||||
this.overlay.style.left = '0px';
|
||||
this.overlay.style.top = '0px';
|
||||
this.overlay.style.backgroundColor = 'rgba(0,0,0,0.0)';
|
||||
|
||||
this.focusPageLast = this.createFocusable();
|
||||
this.overlay.appendChild(this.focusPageLast);
|
||||
|
||||
this.overlay.addEventListener('click', function(e) {
|
||||
var redirectedEvent = document.createEvent('MouseEvents');
|
||||
redirectedEvent.initMouseEvent(e.type, e.bubbles, e.cancelable, window,
|
||||
e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey,
|
||||
e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget);
|
||||
document.body.dispatchEvent(redirectedEvent);
|
||||
});
|
||||
|
||||
// TODO: Only install when any dialogs are open.
|
||||
document.addEventListener('submit', function(ev) {
|
||||
var method = ev.target.getAttribute('method');
|
||||
method = method ? method.toLowerCase() : '';
|
||||
if (method != 'dialog') { return; }
|
||||
ev.preventDefault();
|
||||
|
||||
var dialog = findNearestDialog(ev.target);
|
||||
if (!dialog) { return; }
|
||||
|
||||
// FIXME: The original event doesn't contain the INPUT element used to
|
||||
// submit the form (if any). Look in some possible places.
|
||||
var returnValue;
|
||||
var cands = [document.activeElement, ev.explicitOriginalTarget];
|
||||
cands.some(function(cand) {
|
||||
if (cand && cand.nodeName == 'INPUT' && cand.form == ev.target) {
|
||||
returnValue = cand.value;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
dialog.close(returnValue);
|
||||
}, true);
|
||||
};
|
||||
|
||||
dialogPolyfill.DialogManager.prototype.createFocusable = function(tabIndex) {
|
||||
var span = document.createElement('span');
|
||||
span.tabIndex = tabIndex || 0;
|
||||
span.style.opacity = 0;
|
||||
span.style.position = 'static';
|
||||
return span;
|
||||
};
|
||||
|
||||
dialogPolyfill.DialogManager.prototype.blockDocument = function() {
|
||||
if (!document.body.contains(this.overlay)) {
|
||||
document.body.appendChild(this.overlay);
|
||||
|
||||
// On Safari/Mac (and possibly other browsers), the documentElement is
|
||||
// not focusable. This is required for modal dialogs as it is the first
|
||||
// element to be hit by a tab event, and further tabs are redirected to
|
||||
// the most visible dialog.
|
||||
if (this.needsDocumentElementFocus === undefined) {
|
||||
document.documentElement.focus();
|
||||
this.needsDocumentElementFocus =
|
||||
(document.activeElement != document.documentElement);
|
||||
}
|
||||
if (this.needsDocumentElementFocus) {
|
||||
document.documentElement.tabIndex = 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
dialogPolyfill.DialogManager.prototype.unblockDocument = function() {
|
||||
document.body.removeChild(this.overlay);
|
||||
if (this.needsDocumentElementFocus) {
|
||||
// TODO: Restore the previous tabIndex, rather than clearing it.
|
||||
document.documentElement.tabIndex = '';
|
||||
}
|
||||
};
|
||||
|
||||
dialogPolyfill.DialogManager.prototype.updateStacking = function() {
|
||||
if (this.pendingDialogStack.length == 0) {
|
||||
this.unblockDocument();
|
||||
return;
|
||||
}
|
||||
this.blockDocument();
|
||||
|
||||
var zIndex = TOP_LAYER_ZINDEX;
|
||||
for (var i = 0; i < this.pendingDialogStack.length; i++) {
|
||||
if (i == this.pendingDialogStack.length - 1)
|
||||
this.overlay.style.zIndex = zIndex++;
|
||||
var dialog = this.pendingDialogStack[i];
|
||||
dialog.dialogPolyfillInfo.backdrop.style.zIndex = zIndex++;
|
||||
dialog.style.zIndex = zIndex++;
|
||||
}
|
||||
};
|
||||
|
||||
dialogPolyfill.DialogManager.prototype.handleKey = function(event) {
|
||||
var dialogCount = this.pendingDialogStack.length;
|
||||
if (dialogCount == 0) {
|
||||
return;
|
||||
}
|
||||
var dialog = this.pendingDialogStack[dialogCount - 1];
|
||||
var pfi = dialog.dialogPolyfillInfo;
|
||||
|
||||
switch (event.keyCode) {
|
||||
case 9: /* tab */
|
||||
var activeElement = document.activeElement;
|
||||
var forward = !event.shiftKey;
|
||||
if (forward) {
|
||||
// Tab forward, so look for document or fake last focus element.
|
||||
if (activeElement == document.documentElement ||
|
||||
activeElement == document.body ||
|
||||
activeElement == pfi.backdrop) {
|
||||
pfi.focusFirst.focus();
|
||||
} else if (activeElement == pfi.focusLast) {
|
||||
// TODO: Instead of wrapping to focusFirst, escape to browser chrome.
|
||||
pfi.focusFirst.focus();
|
||||
}
|
||||
} else {
|
||||
// Tab backwards, so look for fake first focus element.
|
||||
if (activeElement == pfi.focusFirst) {
|
||||
// TODO: Instead of wrapping to focusLast, escape to browser chrome.
|
||||
pfi.focusLast.focus();
|
||||
} else if (activeElement == this.focusPageLast) {
|
||||
// The focus element is at the end of the page (e.g., shift-tab from
|
||||
// the window chrome): move current focus to the last element in the
|
||||
// dialog instead.
|
||||
pfi.focusLast.focus();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 27: /* esc */
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
var cancelEvent = new supportCustomEvent('cancel', {
|
||||
bubbles: false,
|
||||
cancelable: true
|
||||
});
|
||||
if (dialog.dispatchEvent(cancelEvent)) {
|
||||
dialog.close();
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
dialogPolyfill.DialogManager.prototype.pushDialog = function(dialog) {
|
||||
if (this.pendingDialogStack.length >= MAX_PENDING_DIALOGS) {
|
||||
throw "Too many modal dialogs";
|
||||
}
|
||||
|
||||
var backdrop = document.createElement('div');
|
||||
backdrop.className = 'backdrop';
|
||||
var clickEventListener = function(e) {
|
||||
var redirectedEvent = document.createEvent('MouseEvents');
|
||||
redirectedEvent.initMouseEvent(e.type, e.bubbles, e.cancelable, window,
|
||||
e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey,
|
||||
e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget);
|
||||
dialog.dispatchEvent(redirectedEvent);
|
||||
};
|
||||
backdrop.addEventListener('click', clickEventListener);
|
||||
dialog.parentNode.insertBefore(backdrop, dialog.nextSibling);
|
||||
dialog.dialogPolyfillInfo.backdrop = backdrop;
|
||||
dialog.dialogPolyfillInfo.clickEventListener = clickEventListener;
|
||||
this.pendingDialogStack.push(dialog);
|
||||
this.updateStacking();
|
||||
|
||||
dialog.dialogPolyfillInfo.focusFirst = this.createFocusable();
|
||||
dialog.dialogPolyfillInfo.focusLast = this.createFocusable();
|
||||
dialog.appendChild(dialog.dialogPolyfillInfo.focusLast);
|
||||
dialog.insertBefore(
|
||||
dialog.dialogPolyfillInfo.focusFirst, dialog.firstChild);
|
||||
};
|
||||
|
||||
dialogPolyfill.DialogManager.prototype.removeDialog = function(dialog) {
|
||||
var index = this.pendingDialogStack.indexOf(dialog);
|
||||
if (index == -1) {
|
||||
return;
|
||||
}
|
||||
this.pendingDialogStack.splice(index, 1);
|
||||
var backdrop = dialog.dialogPolyfillInfo.backdrop;
|
||||
var clickEventListener = dialog.dialogPolyfillInfo.clickEventListener;
|
||||
backdrop.removeEventListener('click', clickEventListener);
|
||||
backdrop.parentNode.removeChild(backdrop);
|
||||
dialog.dialogPolyfillInfo.backdrop = null;
|
||||
dialog.dialogPolyfillInfo.clickEventListener = null;
|
||||
this.updateStacking();
|
||||
|
||||
dialog.removeChild(dialog.dialogPolyfillInfo.focusFirst);
|
||||
dialog.removeChild(dialog.dialogPolyfillInfo.focusLast);
|
||||
dialog.dialogPolyfillInfo.focusFirst = null;
|
||||
dialog.dialogPolyfillInfo.focusLast = null;
|
||||
};
|
||||
|
||||
dialogPolyfill.dm = new dialogPolyfill.DialogManager();
|
||||
|
||||
document.addEventListener('keydown',
|
||||
dialogPolyfill.dm.handleKey.bind(dialogPolyfill.dm));
|
||||
|
||||
return dialogPolyfill;
|
||||
})();
|
||||
@@ -1,34 +0,0 @@
|
||||
dialog
|
||||
position: absolute
|
||||
left: 0
|
||||
right: 0
|
||||
width: -moz-fit-content
|
||||
width: -webkit-fit-content
|
||||
width: fit-content
|
||||
height: -moz-fit-content
|
||||
height: -webkit-fit-content
|
||||
height: fit-content
|
||||
margin: auto
|
||||
border: solid
|
||||
padding: 1em
|
||||
background: white
|
||||
color: black
|
||||
display: none
|
||||
|
||||
dialog[open]
|
||||
display: block
|
||||
|
||||
dialog + .backdrop
|
||||
position: fixed
|
||||
top: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
background: rgba(0,0,0,0.1)
|
||||
|
||||
/* for small devices, modal dialogs go full-screen */
|
||||
@media screen and (max-width: 540px)
|
||||
dialog[_polyfill_modal]
|
||||
top: 0
|
||||
width: auto
|
||||
margin: 1em
|
||||
@@ -1,204 +0,0 @@
|
||||
/*jslint browser: true*/
|
||||
/*jslint jquery: true*/
|
||||
|
||||
/*
|
||||
* jQuery Hotkeys Plugin
|
||||
* Copyright 2010, John Resig
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
*
|
||||
* Based upon the plugin by Tzury Bar Yochay:
|
||||
* https://github.com/tzuryby/jquery.hotkeys
|
||||
*
|
||||
* Original idea by:
|
||||
* Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/
|
||||
*/
|
||||
|
||||
/*
|
||||
* One small change is: now keys are passed by object { keys: '...' }
|
||||
* Might be useful, when you want to pass some other data to your handler
|
||||
*/
|
||||
|
||||
(function(jQuery) {
|
||||
|
||||
jQuery.hotkeys = {
|
||||
version: "0.8",
|
||||
|
||||
specialKeys: {
|
||||
8: "backspace",
|
||||
9: "tab",
|
||||
10: "return",
|
||||
13: "return",
|
||||
16: "shift",
|
||||
17: "ctrl",
|
||||
18: "alt",
|
||||
19: "pause",
|
||||
20: "capslock",
|
||||
27: "esc",
|
||||
32: "space",
|
||||
33: "pageup",
|
||||
34: "pagedown",
|
||||
35: "end",
|
||||
36: "home",
|
||||
37: "left",
|
||||
38: "up",
|
||||
39: "right",
|
||||
40: "down",
|
||||
45: "insert",
|
||||
46: "del",
|
||||
59: ";",
|
||||
61: "=",
|
||||
96: "0",
|
||||
97: "1",
|
||||
98: "2",
|
||||
99: "3",
|
||||
100: "4",
|
||||
101: "5",
|
||||
102: "6",
|
||||
103: "7",
|
||||
104: "8",
|
||||
105: "9",
|
||||
106: "*",
|
||||
107: "+",
|
||||
109: "-",
|
||||
110: ".",
|
||||
111: "/",
|
||||
112: "f1",
|
||||
113: "f2",
|
||||
114: "f3",
|
||||
115: "f4",
|
||||
116: "f5",
|
||||
117: "f6",
|
||||
118: "f7",
|
||||
119: "f8",
|
||||
120: "f9",
|
||||
121: "f10",
|
||||
122: "f11",
|
||||
123: "f12",
|
||||
144: "numlock",
|
||||
145: "scroll",
|
||||
173: "-",
|
||||
186: ";",
|
||||
187: "=",
|
||||
188: ",",
|
||||
189: "-",
|
||||
190: ".",
|
||||
191: "/",
|
||||
192: "`",
|
||||
219: "[",
|
||||
220: "\\",
|
||||
221: "]",
|
||||
222: "'"
|
||||
},
|
||||
|
||||
shiftNums: {
|
||||
"`": "~",
|
||||
"1": "!",
|
||||
"2": "@",
|
||||
"3": "#",
|
||||
"4": "$",
|
||||
"5": "%",
|
||||
"6": "^",
|
||||
"7": "&",
|
||||
"8": "*",
|
||||
"9": "(",
|
||||
"0": ")",
|
||||
"-": "_",
|
||||
"=": "+",
|
||||
";": ": ",
|
||||
"'": "\"",
|
||||
",": "<",
|
||||
".": ">",
|
||||
"/": "?",
|
||||
"\\": "|"
|
||||
},
|
||||
|
||||
// excludes: button, checkbox, file, hidden, image, password, radio, reset, search, submit, url
|
||||
textAcceptingInputTypes: [
|
||||
"text", "password", "number", "email", "url", "range", "date", "month", "week", "time", "datetime",
|
||||
"datetime-local", "search", "color", "tel"],
|
||||
|
||||
// default input types not to bind to unless bound directly
|
||||
textInputTypes: /textarea|input|select/i,
|
||||
|
||||
options: {
|
||||
filterInputAcceptingElements: true,
|
||||
filterTextInputs: true,
|
||||
filterContentEditable: true
|
||||
}
|
||||
};
|
||||
|
||||
function keyHandler(handleObj) {
|
||||
if (typeof handleObj.data === "string") {
|
||||
handleObj.data = {
|
||||
keys: handleObj.data
|
||||
};
|
||||
}
|
||||
|
||||
// Only care when a possible input has been specified
|
||||
if (!handleObj.data || !handleObj.data.keys || typeof handleObj.data.keys !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
var origHandler = handleObj.handler,
|
||||
keys = handleObj.data.keys.toLowerCase().split(" ");
|
||||
|
||||
handleObj.handler = function(event) {
|
||||
// Don't fire in text-accepting inputs that we didn't directly bind to
|
||||
if (this !== event.target &&
|
||||
(jQuery.hotkeys.options.filterInputAcceptingElements &&
|
||||
jQuery.hotkeys.textInputTypes.test(event.target.nodeName) ||
|
||||
(jQuery.hotkeys.options.filterContentEditable && jQuery(event.target).attr('contenteditable')) ||
|
||||
(jQuery.hotkeys.options.filterTextInputs &&
|
||||
jQuery.inArray(event.target.type, jQuery.hotkeys.textAcceptingInputTypes) > -1))) {
|
||||
return;
|
||||
}
|
||||
|
||||
var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[event.which],
|
||||
character = String.fromCharCode(event.which).toLowerCase(),
|
||||
modif = "",
|
||||
possible = {};
|
||||
|
||||
jQuery.each(["alt", "ctrl", "shift"], function(index, specialKey) {
|
||||
|
||||
if (event[specialKey + 'Key'] && special !== specialKey) {
|
||||
modif += specialKey + '+';
|
||||
}
|
||||
});
|
||||
|
||||
// metaKey is triggered off ctrlKey erronously
|
||||
if (event.metaKey && !event.ctrlKey && special !== "meta") {
|
||||
modif += "meta+";
|
||||
}
|
||||
|
||||
if (event.metaKey && special !== "meta" && modif.indexOf("alt+ctrl+shift+") > -1) {
|
||||
modif = modif.replace("alt+ctrl+shift+", "hyper+");
|
||||
}
|
||||
|
||||
if (special) {
|
||||
possible[modif + special] = true;
|
||||
}
|
||||
else {
|
||||
possible[modif + character] = true;
|
||||
possible[modif + jQuery.hotkeys.shiftNums[character]] = true;
|
||||
|
||||
// "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
|
||||
if (modif === "shift+") {
|
||||
possible[jQuery.hotkeys.shiftNums[character]] = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0, l = keys.length; i < l; i++) {
|
||||
if (possible[keys[i]]) {
|
||||
return origHandler.apply(this, arguments);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
jQuery.each(["keydown", "keyup", "keypress"], function() {
|
||||
jQuery.event.special[this] = {
|
||||
add: keyHandler
|
||||
};
|
||||
});
|
||||
|
||||
})(jQuery || this.jQuery || window.jQuery);
|
||||
@@ -1,99 +0,0 @@
|
||||
<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/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>
|
||||
@@ -1,5 +0,0 @@
|
||||
Template.aboutModal.helpers({
|
||||
githubUrl() {
|
||||
return 'https://github.com/OHIF/Viewers';
|
||||
}
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
@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
|
||||
@@ -1,3 +0,0 @@
|
||||
<template name="layout">
|
||||
{{>yield }}
|
||||
</template>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template name="loadingText">
|
||||
<div class="loadingTextDiv">
|
||||
Loading... <i class="fa fa-spin fa-circle-o-notch fa-fw"></i>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,4 +0,0 @@
|
||||
.loadingTextDiv
|
||||
margin: 30px
|
||||
text-align: center
|
||||
font-weight: 300
|
||||
@@ -1,7 +0,0 @@
|
||||
<template name="removableBackdrop">
|
||||
<div class="removableBackdrop noselect"
|
||||
oncontextmenu='return false;'
|
||||
unselectable='on'
|
||||
onselectstart='return false;'>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,9 +0,0 @@
|
||||
.removableBackdrop
|
||||
position: fixed
|
||||
top: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
z-index: 50
|
||||
background-color: transparent
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<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>
|
||||
@@ -1,87 +0,0 @@
|
||||
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 + '%';
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
@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
|
||||
@@ -1,11 +0,0 @@
|
||||
<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>
|
||||
@@ -1,7 +0,0 @@
|
||||
Template.studyBrowser.helpers({
|
||||
studies() {
|
||||
return ViewerStudies.find({
|
||||
selected: true
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
.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
|
||||
@@ -1,30 +0,0 @@
|
||||
<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>
|
||||
@@ -1,279 +0,0 @@
|
||||
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;
|
||||
}
|
||||
});
|
||||
@@ -1,98 +0,0 @@
|
||||
@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
|
||||
@@ -1,33 +0,0 @@
|
||||
<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>
|
||||
@@ -1,126 +0,0 @@
|
||||
// ------ 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));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
@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
|
||||
@@ -1,45 +0,0 @@
|
||||
<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>
|
||||
@@ -1,278 +0,0 @@
|
||||
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'
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1,136 +0,0 @@
|
||||
@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
@@ -1,16 +0,0 @@
|
||||
<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
@@ -1,85 +0,0 @@
|
||||
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
@@ -1,25 +0,0 @@
|
||||
#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
@@ -1,16 +0,0 @@
|
||||
<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
@@ -1,27 +0,0 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
<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>
|
||||
@@ -1,47 +0,0 @@
|
||||
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;
|
||||
}
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
@import "{design}/app.styl"
|
||||
|
||||
#imageViewerViewports
|
||||
height: 100%
|
||||
padding-bottom: 1px
|
||||
padding-right: 6px
|
||||
width: 100%
|
||||
@@ -1,11 +0,0 @@
|
||||
<template name="imageControls">
|
||||
<div class="imageControls">
|
||||
<div id="scrollbar">
|
||||
<input id="imageSlider"
|
||||
type="range"
|
||||
min="1"
|
||||
value="{{imageIndex}}"
|
||||
max="{{numImages}}"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,57 +0,0 @@
|
||||
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;
|
||||
}
|
||||
});
|
||||
@@ -1,115 +0,0 @@
|
||||
@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
@@ -1,14 +0,0 @@
|
||||
<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>
|
||||
-554
@@ -1,554 +0,0 @@
|
||||
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 worklist tab, if the viewport is running
|
||||
// alongside the worklist 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);
|
||||
}
|
||||
|
||||
// Temporary until we have a real window manager with events for series/study changed
|
||||
log.info('Set NewSeriesLoaded');
|
||||
Session.set('NewSeriesLoaded', Random.id());
|
||||
|
||||
// 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
@@ -1,37 +0,0 @@
|
||||
@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
|
||||
@@ -1,20 +0,0 @@
|
||||
<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>
|
||||
@@ -1,17 +0,0 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
<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>
|
||||
@@ -1,87 +0,0 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
@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')
|
||||
@@ -1,14 +0,0 @@
|
||||
<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>
|
||||
@@ -1,72 +0,0 @@
|
||||
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 + '%';
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
@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%
|
||||
@@ -1,16 +0,0 @@
|
||||
<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>
|
||||
@@ -1,8 +0,0 @@
|
||||
Template.playClipButton.helpers({
|
||||
isPlaying: function() {
|
||||
return isPlaying();
|
||||
},
|
||||
disableButton() {
|
||||
return hasMultipleFrames();
|
||||
}
|
||||
});
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
<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
@@ -1,54 +0,0 @@
|
||||
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
@@ -1,61 +0,0 @@
|
||||
@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
@@ -1,30 +0,0 @@
|
||||
<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/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
@@ -1,8 +0,0 @@
|
||||
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
@@ -1,34 +0,0 @@
|
||||
@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))
|
||||
@@ -1,6 +0,0 @@
|
||||
<template name="viewerMain">
|
||||
<div class="viewerMain">
|
||||
<div id='layoutManagerTarget'>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,39 +0,0 @@
|
||||
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();
|
||||
|
||||
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;
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
@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
@@ -1,8 +0,0 @@
|
||||
<template name="viewportOrientationMarkers">
|
||||
<div class="viewportOrientationMarkers noselect">
|
||||
<div class="topMid orientationMarker">
|
||||
</div>
|
||||
<div class="leftMid orientationMarker">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
/**
|
||||
* 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
@@ -1,18 +0,0 @@
|
||||
.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
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
<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>
|
||||
@@ -1,337 +0,0 @@
|
||||
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';
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
@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
|
||||
@@ -1,28 +0,0 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
applyWLPreset = function(presetName, element) {
|
||||
log.info("Applying WL Preset: " + presetName);
|
||||
var viewport = cornerstone.getViewport(element);
|
||||
|
||||
if (presetName === 'Default') {
|
||||
var enabledElement = cornerstone.getEnabledElement(element);
|
||||
viewport.voi.windowWidth = enabledElement.image.windowWidth;
|
||||
viewport.voi.windowCenter = enabledElement.image.windowCenter;
|
||||
} else {
|
||||
var preset = OHIF.viewer.wlPresets[presetName];
|
||||
viewport.voi.windowWidth = preset.ww;
|
||||
viewport.voi.windowCenter = preset.wc;
|
||||
}
|
||||
|
||||
// Update the viewport
|
||||
cornerstone.setViewport(element, viewport);
|
||||
};
|
||||
|
||||
applyWLPresetToActiveElement = function(presetName) {
|
||||
var element = getActiveViewportElement();
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyWLPreset(presetName, element);
|
||||
};
|
||||
@@ -1,123 +0,0 @@
|
||||
/**
|
||||
* Creates a set of series to be placed in the Study Browser
|
||||
* The series that appear in the Study Browser must represent
|
||||
* imaging modalities.
|
||||
*
|
||||
* Furthermore, for drag/drop functionality,
|
||||
* it is easiest if the stack objects also contain information about
|
||||
* which study they are linked to.
|
||||
*
|
||||
* @param study The study instance to be used
|
||||
* @returns {Array} An array of series to be placed in the Study Browser
|
||||
*/
|
||||
createStacks = function(study) {
|
||||
// Define an empty array of display sets
|
||||
var displaySets = [];
|
||||
|
||||
if (!study || !study.seriesList) {
|
||||
return displaySets;
|
||||
}
|
||||
|
||||
study.seriesList.forEach(series => {
|
||||
// If the series has no instances, skip it
|
||||
if (!series.instances) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Search through the instances of this series
|
||||
// Split Multi-frame instances and Single-image modalities
|
||||
// into their own specific display sets. Place the rest of each
|
||||
// series into another display set.
|
||||
let stackableInstances = [];
|
||||
series.instances.forEach(instance => {
|
||||
// All imaging modalities must have a valid value for sopClassUid or rows
|
||||
if (!isImage(instance.sopClassUid) && !instance.rows) {
|
||||
return;
|
||||
}
|
||||
|
||||
let displaySet;
|
||||
if (isMultiFrame(instance)) {
|
||||
displaySet = makeDisplaySet(series, [ instance ]);
|
||||
displaySet.isClip = true;
|
||||
|
||||
// Include the study instance Uid for drag/drop purposes
|
||||
displaySet.studyInstanceUid = study.studyInstanceUid;
|
||||
|
||||
// Override the default value of instances.length
|
||||
displaySet.numImageFrames = instance.numFrames;
|
||||
|
||||
// Include the instance number
|
||||
displaySet.instanceNumber = instance.instanceNumber;
|
||||
|
||||
// Include the acquisition datetime
|
||||
displaySet.acquisitionDatetime = instance.acquisitionDatetime;
|
||||
|
||||
displaySets.push(displaySet);
|
||||
} else if (isSingleImageModality(instance.modality)) {
|
||||
displaySet = makeDisplaySet(series, [ instance ]);
|
||||
|
||||
// Include the study instance Uid
|
||||
displaySet.studyInstanceUid = study.studyInstanceUid;
|
||||
|
||||
// Include the instance number
|
||||
displaySet.instanceNumber = instance.instanceNumber;
|
||||
|
||||
// Include the acquisition datetime
|
||||
displaySet.acquisitionDatetime = instance.acquisitionDatetime;
|
||||
|
||||
displaySets.push(displaySet);
|
||||
} else {
|
||||
stackableInstances.push(instance);
|
||||
}
|
||||
});
|
||||
|
||||
if (stackableInstances.length) {
|
||||
let displaySet = makeDisplaySet(series, stackableInstances);
|
||||
displaySet.studyInstanceUid = study.studyInstanceUid;
|
||||
displaySets.push(displaySet);
|
||||
}
|
||||
});
|
||||
|
||||
return displaySets;
|
||||
};
|
||||
|
||||
function makeDisplaySet(series, instances) {
|
||||
const instance = instances[0];
|
||||
|
||||
let displaySet = {
|
||||
seriesInstanceUid: series.seriesInstanceUid,
|
||||
seriesNumber: series.seriesNumber,
|
||||
seriesDescription: series.seriesDescription,
|
||||
numImageFrames: instances.length,
|
||||
frameRate: instance.frameTime,
|
||||
images: instances,
|
||||
modality: series.modality,
|
||||
isMultiFrame: isMultiFrame(instance)
|
||||
};
|
||||
|
||||
// Sort the images in this series
|
||||
displaySet.images.sort(function(a, b) {
|
||||
if (a.instanceNumber && b.instanceNumber &&
|
||||
a.instanceNumber !== b.instanceNumber) {
|
||||
return a.instanceNumber - b.instanceNumber;
|
||||
}
|
||||
});
|
||||
|
||||
// Include the first image instance number
|
||||
displaySet.instanceNumber = displaySet.images[0].instanceNumber;
|
||||
|
||||
// Create a unique ID for this stack so we can reference it
|
||||
displaySet.displaySetInstanceUid = Random.id();
|
||||
|
||||
return displaySet;
|
||||
}
|
||||
|
||||
function isSingleImageModality(modality) {
|
||||
return (modality === 'CR' ||
|
||||
modality === 'MG' ||
|
||||
modality === 'DX');
|
||||
}
|
||||
|
||||
function isMultiFrame(instance) {
|
||||
return instance.numFrames > 1;
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
Meteor.startup(function() {
|
||||
var debug = false;
|
||||
|
||||
if (debug === true) {
|
||||
// http://www.meteorpedia.com/read/Debugging_Reactivity
|
||||
|
||||
Meteor.autorun(function(computation) {
|
||||
computation.onInvalidate(function() {
|
||||
console.trace();
|
||||
});
|
||||
});
|
||||
|
||||
var wrappedFind = Meteor.Collection.prototype.find;
|
||||
|
||||
Meteor.Collection.prototype.find = function() {
|
||||
var cursor = wrappedFind.apply(this, arguments);
|
||||
var collectionName = this._name || this._debugName;
|
||||
|
||||
/*cursor.observeChanges({
|
||||
added: function(id, fields) {
|
||||
console.log(collectionName, 'added', id, fields);
|
||||
},
|
||||
changed: function(id, fields) {
|
||||
console.log(collectionName, 'changed', id, fields);
|
||||
},
|
||||
movedBefore: function(id, before) {
|
||||
console.log(collectionName, 'movedBefore', id, before);
|
||||
},
|
||||
removed: function(id) {
|
||||
console.log(collectionName, 'removed', id);
|
||||
}
|
||||
});*/
|
||||
|
||||
cursor.observe({
|
||||
added: function(data) {
|
||||
console.log(collectionName, 'added', data);
|
||||
},
|
||||
changed: function(data) {
|
||||
console.log(collectionName, 'changed', data);
|
||||
},
|
||||
removed: function(data) {
|
||||
console.log(collectionName, 'removed', data);
|
||||
}
|
||||
});
|
||||
|
||||
return cursor;
|
||||
};
|
||||
|
||||
function logRenders() {
|
||||
Object.keys(Template).forEach(function(name) {
|
||||
if (name.indexOf('_') > -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var template = Template[name];
|
||||
var oldRender = template.rendered;
|
||||
var counter = 0;
|
||||
|
||||
template.rendered = function() {
|
||||
console.log(name, 'render count: ', ++counter);
|
||||
oldRender && oldRender.apply(this, arguments);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
logRenders();
|
||||
}
|
||||
});
|
||||
@@ -1,43 +0,0 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
/**
|
||||
* This function disables reference lines for a specific viewport element.
|
||||
* It also enables reference lines for all other viewports with the
|
||||
* class .imageViewerViewport.
|
||||
*
|
||||
* @param element {node} DOM Node representing the viewport element
|
||||
*/
|
||||
displayReferenceLines = element => {
|
||||
log.info("imageViewerViewport displayReferenceLines");
|
||||
|
||||
// Check if image plane (orientation / loction) data is present for the current image
|
||||
const enabledElement = cornerstone.getEnabledElement(element);
|
||||
const imageId = enabledElement.image.imageId;
|
||||
const imagePlane = cornerstoneTools.metaData.get('imagePlane', imageId);
|
||||
|
||||
if (!OHIF.viewer.refLinesEnabled || !imagePlane || !imagePlane.frameOfReferenceUID) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable reference lines for the current element
|
||||
cornerstoneTools.referenceLines.tool.disable(element);
|
||||
|
||||
// Loop through all other viewport elements and enable reference lines
|
||||
$('.imageViewerViewport').not(element).each((index, viewportElement) => {
|
||||
let imageId;
|
||||
if($(viewportElement).find('canvas').length) {
|
||||
try {
|
||||
const enabledElement = cornerstone.getEnabledElement(viewportElement);
|
||||
imageId = enabledElement.image.imageId;
|
||||
} catch(error) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!imageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
cornerstoneTools.referenceLines.tool.enable(viewportElement, OHIF.viewer.updateImageSynchronizer);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,309 +0,0 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Meteor.startup(function() {
|
||||
|
||||
if (!OHIF.viewer) {
|
||||
OHIF.viewer = {};
|
||||
}
|
||||
|
||||
OHIF.viewer.defaultHotkeys = {
|
||||
defaultTool: 'ESC',
|
||||
angle: 'A',
|
||||
stackScroll: 'S',
|
||||
pan: 'P',
|
||||
magnify: 'M',
|
||||
scrollDown: 'DOWN',
|
||||
scrollUp: 'UP',
|
||||
nextDisplaySet: 'PAGEDOWN',
|
||||
previousDisplaySet: 'PAGEUP',
|
||||
nextPanel: 'RIGHT',
|
||||
previousPanel: 'LEFT',
|
||||
invert: 'I',
|
||||
flipV: 'V',
|
||||
flipH: 'H',
|
||||
wwwc: 'W',
|
||||
zoom: 'Z',
|
||||
cinePlay: 'SPACE',
|
||||
rotateR: 'R',
|
||||
rotateL: 'L',
|
||||
toggleOverlayTags: 'SHIFT',
|
||||
WLPresetSoftTissue: ['NUMPAD1', '1'],
|
||||
WLPresetLung: ['NUMPAD2', '2'],
|
||||
WLPresetLiver: ['NUMPAD3', '3'],
|
||||
WLPresetBone: ['NUMPAD4', '4'],
|
||||
WLPresetBrain: ['NUMPAD5', '5']
|
||||
};
|
||||
|
||||
// For now
|
||||
OHIF.viewer.hotkeys = OHIF.viewer.defaultHotkeys;
|
||||
|
||||
OHIF.viewer.defaultWLPresets = {
|
||||
SoftTissue: {
|
||||
wc: 40,
|
||||
ww: 400
|
||||
},
|
||||
Lung: {
|
||||
wc: -600,
|
||||
ww: 1500
|
||||
},
|
||||
Liver: {
|
||||
wc: 90,
|
||||
ww: 150
|
||||
},
|
||||
Bone: {
|
||||
wc: 480,
|
||||
ww: 2500
|
||||
},
|
||||
Brain: {
|
||||
wc: 40,
|
||||
ww: 80
|
||||
}
|
||||
};
|
||||
|
||||
// For now
|
||||
OHIF.viewer.wlPresets = OHIF.viewer.defaultWLPresets;
|
||||
|
||||
OHIF.viewer.hotkeyFunctions = {
|
||||
wwwc: function() {
|
||||
toolManager.setActiveTool('wwwc');
|
||||
},
|
||||
zoom: function() {
|
||||
toolManager.setActiveTool('zoom');
|
||||
},
|
||||
angle: function() {
|
||||
toolManager.setActiveTool('angle');
|
||||
},
|
||||
dragProbe: function() {
|
||||
toolManager.setActiveTool('dragProbe');
|
||||
},
|
||||
ellipticalRoi: function() {
|
||||
toolManager.setActiveTool('ellipticalRoi');
|
||||
},
|
||||
magnify: function() {
|
||||
toolManager.setActiveTool('magnify');
|
||||
},
|
||||
annotate: function() {
|
||||
toolManager.setActiveTool('annotate');
|
||||
},
|
||||
stackScroll: function() {
|
||||
toolManager.setActiveTool('stackScroll');
|
||||
},
|
||||
pan: function() {
|
||||
toolManager.setActiveTool('pan');
|
||||
},
|
||||
length: function() {
|
||||
toolManager.setActiveTool('length');
|
||||
},
|
||||
spine: function() {
|
||||
toolManager.setActiveTool('spine');
|
||||
},
|
||||
wwwcRegion: function() {
|
||||
toolManager.setActiveTool('wwwcRegion');
|
||||
},
|
||||
zoomIn: function() {
|
||||
var button = document.getElementById('zoomIn');
|
||||
flashButton(button);
|
||||
zoomIn();
|
||||
},
|
||||
zoomOut: function() {
|
||||
var button = document.getElementById('zoomOut');
|
||||
flashButton(button);
|
||||
zoomOut();
|
||||
},
|
||||
zoomToFit: function() {
|
||||
var button = document.getElementById('zoomToFit');
|
||||
flashButton(button);
|
||||
zoomToFit();
|
||||
},
|
||||
scrollDown: function() {
|
||||
var container = $('.viewportContainer.active');
|
||||
var button = container.find('#nextImage').get(0);
|
||||
|
||||
if (!container.find('.imageViewerViewport').hasClass('empty')) {
|
||||
flashButton(button);
|
||||
switchToImageRelative(1);
|
||||
}
|
||||
},
|
||||
scrollFirstImage: function() {
|
||||
var container = $('.viewportContainer.active');
|
||||
if (!container.find('.imageViewerViewport').hasClass('empty')) {
|
||||
switchToImageByIndex(0);
|
||||
}
|
||||
},
|
||||
scrollLastImage: function() {
|
||||
var container = $('.viewportContainer.active');
|
||||
if (!container.find('.imageViewerViewport').hasClass('empty')) {
|
||||
switchToImageByIndex(-1);
|
||||
}
|
||||
},
|
||||
scrollUp: function() {
|
||||
var container = $('.viewportContainer.active');
|
||||
if (!container.find('.imageViewerViewport').hasClass('empty')) {
|
||||
var button = container.find('#prevImage').get(0);
|
||||
flashButton(button);
|
||||
switchToImageRelative(-1);
|
||||
}
|
||||
},
|
||||
previousDisplaySet() {
|
||||
OHIF.viewer.moveDisplaySets(false);
|
||||
},
|
||||
nextDisplaySet() {
|
||||
OHIF.viewer.moveDisplaySets(true);
|
||||
},
|
||||
nextPanel: function() {
|
||||
nextActivePanel();
|
||||
},
|
||||
previousPanel: function() {
|
||||
previousActivePanel();
|
||||
},
|
||||
invert: function() {
|
||||
var button = document.getElementById('invert');
|
||||
flashButton(button);
|
||||
invert();
|
||||
},
|
||||
flipV: function() {
|
||||
var button = document.getElementById('flipV');
|
||||
flashButton(button);
|
||||
flipV();
|
||||
},
|
||||
flipH: function() {
|
||||
var button = document.getElementById('flipH');
|
||||
flashButton(button);
|
||||
flipH();
|
||||
},
|
||||
rotateR: function() {
|
||||
var button = document.getElementById('rotateR');
|
||||
flashButton(button);
|
||||
rotateR();
|
||||
},
|
||||
rotateL: function() {
|
||||
var button = document.getElementById('rotateL');
|
||||
flashButton(button);
|
||||
rotateL();
|
||||
},
|
||||
cinePlay: function() {
|
||||
toggleCinePlay();
|
||||
},
|
||||
defaultTool: function() {
|
||||
var tool = toolManager.getDefaultTool();
|
||||
toolManager.setActiveTool(tool);
|
||||
},
|
||||
toggleOverlayTags: function() {
|
||||
var dicomTags = $('.imageViewerViewportOverlay .dicomTag');
|
||||
if (dicomTags.eq(0).css('display') === 'none') {
|
||||
dicomTags.show();
|
||||
} else {
|
||||
dicomTags.hide();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
OHIF.viewer.loadedSeriesData = {};
|
||||
});
|
||||
|
||||
// Define a jQuery reverse function
|
||||
$.fn.reverse = [].reverse;
|
||||
|
||||
function previousActivePanel() {
|
||||
log.info('nextActivePanel');
|
||||
var currentIndex = Session.get('activeViewport');
|
||||
currentIndex--;
|
||||
|
||||
var viewports = $('.imageViewerViewport');
|
||||
var numViewports = viewports.length;
|
||||
if (currentIndex < 0) {
|
||||
currentIndex = numViewports - 1;
|
||||
}
|
||||
|
||||
var element = viewports.get(currentIndex);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveViewport(element);
|
||||
}
|
||||
|
||||
function nextActivePanel() {
|
||||
log.info('nextActivePanel');
|
||||
var currentIndex = Session.get('activeViewport');
|
||||
currentIndex++;
|
||||
|
||||
var viewports = $('.imageViewerViewport');
|
||||
var numViewports = viewports.length;
|
||||
if (currentIndex >= numViewports) {
|
||||
currentIndex = 0;
|
||||
}
|
||||
|
||||
var element = viewports.get(currentIndex);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveViewport(element);
|
||||
}
|
||||
|
||||
function flashButton(button) {
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
button.classList.add('active');
|
||||
setTimeout(function() {
|
||||
button.classList.remove('active');
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function bindHotkey(hotkey, task) {
|
||||
var hotkeyFunctions = OHIF.viewer.hotkeyFunctions;
|
||||
|
||||
// Only bind defined, non-empty HotKeys
|
||||
if (!hotkey || hotkey === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
var fn;
|
||||
if (task.indexOf('WLPreset') > -1) {
|
||||
var presetName = task.replace('WLPreset', '');
|
||||
fn = function() {
|
||||
applyWLPresetToActiveElement(presetName);
|
||||
};
|
||||
} else {
|
||||
fn = hotkeyFunctions[task];
|
||||
|
||||
// If the function doesn't exist in the
|
||||
// hotkey function list, try the viewer-specific function list
|
||||
if (!fn && OHIF.viewer && OHIF.viewer.functionList) {
|
||||
fn = OHIF.viewer.functionList[task];
|
||||
}
|
||||
}
|
||||
|
||||
if (!fn) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hotKeyForBinding = hotkey.toLowerCase();
|
||||
|
||||
$(document).bind('keydown', hotKeyForBinding, fn);
|
||||
}
|
||||
|
||||
enableHotkeys = function() {
|
||||
var viewerHotkeys = OHIF.viewer.hotkeys;
|
||||
|
||||
$(document).unbind('keydown');
|
||||
|
||||
Object.keys(viewerHotkeys).forEach(function(task) {
|
||||
var taskHotkeys = viewerHotkeys[task];
|
||||
if (!taskHotkeys || !taskHotkeys.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (taskHotkeys.constructor === Array) {
|
||||
taskHotkeys.forEach(function(hotkey) {
|
||||
bindHotkey(hotkey, task);
|
||||
});
|
||||
} else {
|
||||
// taskHotkeys represents a single key
|
||||
bindHotkey(taskHotkeys, task);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* This function enables stack prefetching for a specified element (viewport)
|
||||
* It first disables any prefetching currently occurring on any other viewports.
|
||||
*
|
||||
* @param element {node} DOM Node representing the viewport element
|
||||
*/
|
||||
enablePrefetchOnElement = function(element) {
|
||||
log.info("imageViewerViewport enablePrefetchOnElement");
|
||||
|
||||
// Loop through all viewports and disable stackPrefetch
|
||||
$('.imageViewerViewport').each(function() {
|
||||
if (!$(this).find('canvas').length) {
|
||||
return;
|
||||
}
|
||||
cornerstoneTools.stackPrefetch.disable(this);
|
||||
});
|
||||
|
||||
// Make sure there is a stack to fetch
|
||||
const stack = cornerstoneTools.getToolState(element, 'stack');
|
||||
if (stack && stack.data.length && stack.data[0].imageIds.length > 1) {
|
||||
cornerstoneTools.stackPrefetch.enable(element);
|
||||
}
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* Obtain an imageId for Cornerstone from an image instance
|
||||
*
|
||||
* @param instance
|
||||
* @returns {string} The imageId to be used by Cornerstone
|
||||
*/
|
||||
getImageId = function(instance, frame) {
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (instance.url) {
|
||||
return instance.url;
|
||||
}
|
||||
|
||||
if (instance.wadouri) {
|
||||
var imageId = 'dicomweb:' + instance.wadouri; // WADO-URI;
|
||||
if (frame !== undefined) {
|
||||
imageId += '&frame=' + frame;
|
||||
}
|
||||
|
||||
return imageId;
|
||||
} else {
|
||||
// TODO= Check multiframe image support with WADO-RS
|
||||
return getWADORSImageId(instance); // WADO-RS Retrieve Frame
|
||||
}
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
/**
|
||||
* Obtain an imageId for Cornerstone based on the WADO-RS scheme
|
||||
*
|
||||
* @param instance
|
||||
* @returns {string} The imageId to be used by Cornerstone
|
||||
*/
|
||||
|
||||
getWADORSImageId = function(instance) {
|
||||
var columnPixelSpacing = 1.0;
|
||||
var rowPixelSpacing = 1.0;
|
||||
if (instance.pixelSpacing) {
|
||||
var split = instance.pixelSpacing.split('\\');
|
||||
rowPixelSpacing = parseFloat(split[0]);
|
||||
columnPixelSpacing = parseFloat(split[1]);
|
||||
}
|
||||
|
||||
var windowWidth;
|
||||
var windowCenter;
|
||||
|
||||
if (instance.windowWidth && instance.windowCenter) {
|
||||
windowWidth = parseFloat(instance.windowWidth.split('\\')[0]);
|
||||
windowCenter = parseFloat(instance.windowCenter.split('\\')[0]);
|
||||
}
|
||||
|
||||
var image = {
|
||||
uri: instance.wadorsuri,
|
||||
//imageId : '',
|
||||
//minPixelValue : 0,
|
||||
//maxPixelValue : 255,
|
||||
slope: instance.rescaleSlope,
|
||||
intercept: instance.rescaleIntercept,
|
||||
samplesPerPixel: instance.samplesPerPixel,
|
||||
imageOrientationPatient: instance.imageOrientationPatient,
|
||||
imagePositionPatient: instance.imagePositionPatient,
|
||||
sopClassUid: instance.sopClassUid,
|
||||
instanceNumber: instance.instanceNumber,
|
||||
frameOfReferenceUID: instance.frameOfReferenceUID,
|
||||
windowCenter: windowCenter,
|
||||
windowWidth: windowWidth,
|
||||
//render: cornerstone.renderColorImage,
|
||||
//getPixelData: getPixelData,
|
||||
//getImageData: getImageData,
|
||||
//getCanvas: getCanvas,
|
||||
rows: instance.rows,
|
||||
columns: instance.columns,
|
||||
height: instance.rows,
|
||||
width: instance.columns,
|
||||
color: false,
|
||||
columnPixelSpacing: columnPixelSpacing,
|
||||
rowPixelSpacing: rowPixelSpacing,
|
||||
invert: false,
|
||||
sizeInBytes: instance.rows * instance.columns * (instance.bitsAllocated / 8),
|
||||
instance: instance
|
||||
};
|
||||
|
||||
var imageId = cornerstoneWADOImageLoader.imageManager.add(image);
|
||||
|
||||
log.info('WADO-RS ImageID: ' + imageId);
|
||||
return imageId;
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* A global Blaze UI helper to capitalizes the first letter of an input String
|
||||
*
|
||||
* Credit to:
|
||||
*
|
||||
* http://stackoverflow.com/questions/1026069/capitalize-the-first-letter-of-string-in-javascript
|
||||
*/
|
||||
UI.registerHelper('capitalizeFirstLetter', function (context) {
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
return context.charAt(0).toUpperCase() + context.slice(1);
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* A global Blaze UI helper function to format DICOM Dates using the Moment library
|
||||
*/
|
||||
UI.registerHelper('formatDA', function (context, format, options) {
|
||||
if (!context) {
|
||||
return undefined;
|
||||
}
|
||||
var dateAsMoment = moment(context, "YYYYMMDD");
|
||||
var strFormat = "MMM D, YYYY";
|
||||
if (options) {
|
||||
strFormat = format;
|
||||
}
|
||||
return dateAsMoment.format(strFormat);
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* A global Blaze UI helper function to format JavaScript Dates using the Moment library
|
||||
*/
|
||||
UI.registerHelper('formatJSDate', function(context, format, options) {
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
var dateAsMoment = moment(new Date(context));
|
||||
var strFormat = 'MMM D, YYYY';
|
||||
if (options) {
|
||||
strFormat = format;
|
||||
}
|
||||
|
||||
return dateAsMoment.format(strFormat);
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* A global Blaze UI helper to format a float value to a specified precision
|
||||
*/
|
||||
UI.registerHelper('formatNumberPrecision', function(context, precision) {
|
||||
if (context) {
|
||||
return parseFloat(context).toFixed(precision);
|
||||
}
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Formats a patient name for display purposes
|
||||
*/
|
||||
formatPN = function(context) {
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
return context.replace('^', ', ');
|
||||
};
|
||||
|
||||
/**
|
||||
* A global Blaze UI helper to format a patient name for display purposes
|
||||
*/
|
||||
UI.registerHelper('formatPN', formatPN);
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* A global Blaze UI helper to format a DICOM Time for display using the Moment library
|
||||
*/
|
||||
UI.registerHelper('formatTM', function(context, options) {
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
// DICOM Time is stored as HHmmss.SSS, where:
|
||||
// HH 24 hour time:
|
||||
// m mm 0..59 Minutes
|
||||
// s ss 0..59 Seconds
|
||||
// S SS SSS 0..999 Fractional seconds
|
||||
//
|
||||
// See MomentJS: http://momentjs.com/docs/#/parsing/string-format/
|
||||
var dateTime = moment(context, 'HHmmss.SSS');
|
||||
|
||||
var format = "HH:mm:ss";
|
||||
if (options && options.format) {
|
||||
format = options.format;
|
||||
}
|
||||
|
||||
return dateTime.format(format);
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* Helper for retrieving username given userId
|
||||
*/
|
||||
UI.registerHelper('getUsername', function(userId) {
|
||||
var user = Meteor.users.findOne({
|
||||
userId: userId
|
||||
});
|
||||
|
||||
if (user && user.name) {
|
||||
return user.name;
|
||||
}
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* Helper for checking datatype of a variable
|
||||
*/
|
||||
UI.registerHelper('ifTypeIs', function(value, match, attributeName) {
|
||||
if (typeof(value) === match) {
|
||||
return attributeName;
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
UI.registerHelper('inc', function(value) {
|
||||
return parseInt(value) + 1;
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* Helper for setting checkboxes as checked or unchecked inside templates,
|
||||
* based on another variable's value
|
||||
*/
|
||||
UI.registerHelper("inlineIf", function (value, match, attributeName) {
|
||||
if(value === match) {
|
||||
return attributeName;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* Boolean helper to identify if a series instance is active in some viewport
|
||||
*/
|
||||
Template.registerHelper('isDisplaySetActive', (displaySetInstanceUid, viewportIndex) => {
|
||||
// Run this computation every time the viewports are updated
|
||||
Session.get('LayoutManagerUpdated');
|
||||
|
||||
// Stop here if layoutManager is not defined yet
|
||||
if (!window.layoutManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the display set is current visible in any of the layout
|
||||
// manager's displayed viewports. Note that we have to check the
|
||||
// onscreen number of viewports here, since the layout manager will
|
||||
// keep the viewport data of old viewports, even after the layout is changed.
|
||||
//
|
||||
// This behaviour is intentional. If the user displays four viewports, then assigns
|
||||
// display sets to them, and then switches to / from another layout configuration,
|
||||
// we don't want them to lose their specified viewports.
|
||||
let result = false;
|
||||
if (_.isUndefined(viewportIndex)) {
|
||||
// Get the number of viewports that are currently displayed
|
||||
// (Note, viewportData may have more entries!)
|
||||
const currentNumberOfViewports = layoutManager.getNumberOfViewports();
|
||||
|
||||
// Loop through the viewport data up until the currently displayed
|
||||
// number of viewports
|
||||
let viewportData = window.layoutManager.viewportData;
|
||||
for (let i = 0; i < currentNumberOfViewports; i++) {
|
||||
const data = viewportData[i];
|
||||
|
||||
// If the display set is displayed in this viewport, stop here
|
||||
if (data && data.displaySetInstanceUid === displaySetInstanceUid) {
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const data = window.layoutManager.viewportData[viewportIndex];
|
||||
|
||||
// If the display set is displayed in this viewport, stop here
|
||||
if (data && data.displaySetInstanceUid === displaySetInstanceUid) {
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
/**
|
||||
* Helper function to determine if the current client devices
|
||||
* is touch-capable. This can be used to modify certain aspects of the UI.
|
||||
*
|
||||
* The check may not work on all devices!
|
||||
*
|
||||
* @returns {boolean} true if the client device is touch-capable, false otherwise
|
||||
*/
|
||||
isTouchDevice = function() {
|
||||
return (('ontouchstart' in window) ||
|
||||
(navigator.MaxTouchPoints > 0) ||
|
||||
(navigator.msMaxTouchPoints > 0));
|
||||
};
|
||||
|
||||
/**
|
||||
* Blaze helper for checking if the current device is touch capable
|
||||
*
|
||||
* @returns {boolean} true if the client device is touch-capable, false otherwise
|
||||
*/
|
||||
UI.registerHelper('isTouchDevice', isTouchDevice);
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* A global Blaze UI helper function to format JavaScript Dates using the Moment library
|
||||
*/
|
||||
UI.registerHelper('jsDateFromNow', function(context, format, options) {
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
Session.get('timeAgoVariable');
|
||||
|
||||
var dateAsMoment = moment(new Date(context));
|
||||
return dateAsMoment.fromNow();
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
UI.registerHelper('objectEach', function(object) {
|
||||
// http://stackoverflow.com/questions/30234732/how-to-print-key-and-values-in-meteor-template
|
||||
return _.map(object, function(value, key) {
|
||||
return _.extend({key: key}, value);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
UI.registerHelper('objectToPairs', function(object) {
|
||||
// http://stackoverflow.com/questions/30234732/how-to-print-key-and-values-in-meteor-template
|
||||
return _.map(object, function(value, key) {
|
||||
return {
|
||||
key: key,
|
||||
value: value
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* A global Blaze UI helper to Stringify a JavaScript object
|
||||
*
|
||||
* Credit to:
|
||||
*
|
||||
* http://stackoverflow.com/questions/1026069/capitalize-the-first-letter-of-string-in-javascript
|
||||
*/
|
||||
UI.registerHelper('prettyPrintStringify', function(context) {
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
var string = JSON.stringify(context, null, 2);
|
||||
string = string.replace(/['"]+/g, '');
|
||||
string = string.replace('{', '');
|
||||
string = string.replace('}', '');
|
||||
string = string.replace(',', '\n');
|
||||
return string;
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
import { _ } from 'meteor/underscore';
|
||||
import { Template } from 'meteor/templating';
|
||||
|
||||
/**
|
||||
* Global Blaze UI helper to sort array elements
|
||||
* by an array element's property (property) or deep object property (property.childProperty)
|
||||
* Sorts ascending as default
|
||||
*/
|
||||
Template.registerHelper('sort', (array, sortBy, sortType) => {
|
||||
if (!sortBy) {
|
||||
return array;
|
||||
}
|
||||
|
||||
// To keep the order for the same values of the field which is used to sort:
|
||||
// 1. Group the array by the field
|
||||
// 2. Sort the grouped array
|
||||
// 3. Ungroup the sorted array
|
||||
|
||||
const groupedArray = _.groupBy(array, (element) => {
|
||||
if (sortBy) {
|
||||
var groupingElement = getKeyValue(element, sortBy);
|
||||
if (groupingElement) {
|
||||
return groupingElement;
|
||||
}
|
||||
}
|
||||
return element;
|
||||
});
|
||||
|
||||
const sortedArray = _.sortBy(groupedArray, (element) => {
|
||||
if (sortBy) {
|
||||
var sortingElement = getKeyValue(element[0], sortBy);
|
||||
if (sortingElement) {
|
||||
return sortingElement;
|
||||
}
|
||||
}
|
||||
return element;
|
||||
});
|
||||
|
||||
if (sortType === 'desc') {
|
||||
return _.flatten(sortedArray.reverse(), true);
|
||||
}
|
||||
|
||||
return _.flatten(sortedArray, true);
|
||||
});
|
||||
|
||||
function getKeyValue(object, keyPath) {
|
||||
keyPath = keyPath.split('.');
|
||||
for (var i = 0; i < keyPath.length; i++) {
|
||||
if (object && _.has(object, keyPath[i])) {
|
||||
object = object[keyPath[i]];
|
||||
}
|
||||
else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return object;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* A global Blaze UI helper to get the thumbnails for the given study
|
||||
*/
|
||||
Template.registerHelper('studyThumbnails', study => {
|
||||
if (!study) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the study's stacks
|
||||
const stacks = study.displaySets;
|
||||
|
||||
// Defines the resulting thumbnails list
|
||||
const thumbnails = [];
|
||||
|
||||
// Iterate over the stacks and add one by one with its index
|
||||
_.each(stacks, (stack, index) => {
|
||||
thumbnails.push({
|
||||
thumbnailIndex: index,
|
||||
stack: stack
|
||||
});
|
||||
});
|
||||
|
||||
return thumbnails;
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
var instanceClassViewportSettingsFunctions = {};
|
||||
getInstanceClassDefaultViewport = function(series, enabledElement, imageId) {
|
||||
var instanceClass = series.sopClassUid;
|
||||
|
||||
if (!instanceClassViewportSettingsFunctions[instanceClass]) {
|
||||
return;
|
||||
}
|
||||
|
||||
return instanceClassViewportSettingsFunctions[instanceClass](series, enabledElement, imageId);
|
||||
};
|
||||
|
||||
setInstanceClassDefaultViewportFunction = function(instanceClass, fn) {
|
||||
instanceClassViewportSettingsFunctions[instanceClass] = fn;
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
/**
|
||||
* Checks whether dicom files with specified SOP Class UID have image data
|
||||
* @param {string} sopClassUid - SOP Class UID to be checked
|
||||
* @returns {boolean} - true if it has image data
|
||||
*/
|
||||
isImage = function(sopClassUid) {
|
||||
if (sopClassUid == sopClassDictionary.ComputedRadiographyImageStorage
|
||||
|| sopClassUid == sopClassDictionary.DigitalXRayImageStorageForPresentation
|
||||
|| sopClassUid == sopClassDictionary.DigitalXRayImageStorageForProcessing
|
||||
|| sopClassUid == sopClassDictionary.DigitalMammographyXRayImageStorageForPresentation
|
||||
|| sopClassUid == sopClassDictionary.DigitalMammographyXRayImageStorageForProcessing
|
||||
|| sopClassUid == sopClassDictionary.DigitalIntraOralXRayImageStorageForPresentation
|
||||
|| sopClassUid == sopClassDictionary.DigitalIntraOralXRayImageStorageForProcessing
|
||||
|| sopClassUid == sopClassDictionary.CTImageStorage
|
||||
|| sopClassUid == sopClassDictionary.EnhancedCTImageStorage
|
||||
|| sopClassUid == sopClassDictionary.LegacyConvertedEnhancedCTImageStorage
|
||||
|| sopClassUid == sopClassDictionary.UltrasoundMultiframeImageStorage
|
||||
|| sopClassUid == sopClassDictionary.MRImageStorage
|
||||
|| sopClassUid == sopClassDictionary.EnhancedMRImageStorage
|
||||
|| sopClassUid == sopClassDictionary.EnhancedMRColorImageStorage
|
||||
|| sopClassUid == sopClassDictionary.LegacyConvertedEnhancedMRImageStorage
|
||||
|| sopClassUid == sopClassDictionary.UltrasoundImageStorage
|
||||
|| sopClassUid == sopClassDictionary.SecondaryCaptureImageStorage
|
||||
|| sopClassUid == sopClassDictionary.MultiframeSingleBitSecondaryCaptureImageStorage
|
||||
|| sopClassUid == sopClassDictionary.MultiframeGrayscaleByteSecondaryCaptureImageStorage
|
||||
|| sopClassUid == sopClassDictionary.MultiframeGrayscaleWordSecondaryCaptureImageStorage
|
||||
|| sopClassUid == sopClassDictionary.MultiframeTrueColorSecondaryCaptureImageStorage
|
||||
|| sopClassUid == sopClassDictionary.XRayAngiographicImageStorage
|
||||
|| sopClassUid == sopClassDictionary.EnhancedXAImageStorage
|
||||
|| sopClassUid == sopClassDictionary.XRayRadiofluoroscopicImageStorage
|
||||
|| sopClassUid == sopClassDictionary.EnhancedXRFImageStorage
|
||||
|| sopClassUid == sopClassDictionary.XRay3DAngiographicImageStorage
|
||||
|| sopClassUid == sopClassDictionary.XRay3DCraniofacialImageStorage
|
||||
|| sopClassUid == sopClassDictionary.BreastTomosynthesisImageStorage
|
||||
|| sopClassUid == sopClassDictionary.BreastProjectionXRayImageStorageForPresentation
|
||||
|| sopClassUid == sopClassDictionary.BreastProjectionXRayImageStorageForProcessing
|
||||
|| sopClassUid == sopClassDictionary.IntravascularOpticalCoherenceTomographyImageStorageForPresentation
|
||||
|| sopClassUid == sopClassDictionary.IntravascularOpticalCoherenceTomographyImageStorageForProcessing
|
||||
|| sopClassUid == sopClassDictionary.NuclearMedicineImageStorage
|
||||
|| sopClassUid == sopClassDictionary.VLEndoscopicImageStorage
|
||||
|| sopClassUid == sopClassDictionary.VideoEndoscopicImageStorage
|
||||
|| sopClassUid == sopClassDictionary.VLMicroscopicImageStorage
|
||||
|| sopClassUid == sopClassDictionary.VideoMicroscopicImageStorage
|
||||
|| sopClassUid == sopClassDictionary.VLSlideCoordinatesMicroscopicImageStorage
|
||||
|| sopClassUid == sopClassDictionary.VLPhotographicImageStorage
|
||||
|| sopClassUid == sopClassDictionary.VideoPhotographicImageStorage
|
||||
|| sopClassUid == sopClassDictionary.OphthalmicPhotography8BitImageStorage
|
||||
|| sopClassUid == sopClassDictionary.OphthalmicPhotography16BitImageStorage
|
||||
|| sopClassUid == sopClassDictionary.OphthalmicTomographyImageStorage
|
||||
|| sopClassUid == sopClassDictionary.VLWholeSlideMicroscopyImageStorage
|
||||
|| sopClassUid == sopClassDictionary.PositronEmissionTomographyImageStorage
|
||||
|| sopClassUid == sopClassDictionary.EnhancedPETImageStorage
|
||||
|| sopClassUid == sopClassDictionary.LegacyConvertedEnhancedPETImageStorage
|
||||
|| sopClassUid == sopClassDictionary.RTImageStorage) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
@@ -1,234 +0,0 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
// Displays Series in Viewports given a Protocol and list of Studies
|
||||
LayoutManager = class LayoutManager {
|
||||
constructor(parentNode, studies) {
|
||||
log.info('LayoutManager');
|
||||
|
||||
this.parentNode = parentNode;
|
||||
this.studies = studies;
|
||||
this.viewportData = [];
|
||||
this.layoutTemplateName = 'gridLayout';
|
||||
this.layoutProps = {
|
||||
rows: 1,
|
||||
columns: 1
|
||||
};
|
||||
|
||||
this.viewportData = [];
|
||||
|
||||
this.isZoomed = false;
|
||||
|
||||
const updateSessionFn = () => Tracker.afterFlush(() => Session.set('LayoutManagerUpdated', Random.id()));
|
||||
this.updateSession = _.throttle(updateSessionFn, 300);
|
||||
}
|
||||
|
||||
getNumberOfViewports() {
|
||||
return this.layoutProps.rows * this.layoutProps.columns;
|
||||
}
|
||||
|
||||
setDefaultViewportData() {
|
||||
const self = this;
|
||||
|
||||
// Get the number of vieports to be rendered
|
||||
const viewportsAmount = this.getNumberOfViewports();
|
||||
|
||||
// Store the old viewport data and reset the current
|
||||
const oldViewportData = self.viewportData;
|
||||
|
||||
// Get the studies and display sets sequence map
|
||||
const sequenceMap = OHIF.viewer.getDisplaySetSequenceMap();
|
||||
|
||||
// Check if the display sets are sequenced
|
||||
const isSequenced = OHIF.viewer.isDisplaySetsSequenced(sequenceMap);
|
||||
|
||||
// Define the current viewport index and the viewport data array
|
||||
let currentViewportIndex = 0;
|
||||
if (viewportsAmount > oldViewportData.length && oldViewportData.length && isSequenced) {
|
||||
// Keep the displayed display sets
|
||||
self.viewportData = oldViewportData;
|
||||
currentViewportIndex = oldViewportData.length - 1;
|
||||
} else if (viewportsAmount <= oldViewportData.length) {
|
||||
// Reduce the original displayed display sets
|
||||
self.viewportData = oldViewportData.slice(0, viewportsAmount);
|
||||
return;
|
||||
} else {
|
||||
// Reset all display sets
|
||||
self.viewportData = [];
|
||||
}
|
||||
|
||||
// Get all the display sets for the viewer studies
|
||||
let displaySets = [];
|
||||
this.studies.forEach(study => {
|
||||
study.displaySets.forEach(displaySet => {
|
||||
displaySet.images.length && displaySets.push(displaySet);
|
||||
});
|
||||
});
|
||||
|
||||
// Get the display sets that will be appended to the current ones
|
||||
let appendix;
|
||||
const currentLength = self.viewportData.length;
|
||||
if (currentLength) {
|
||||
// TODO: isolate displaySets array by study (maybe a map?)
|
||||
const beginIndex = sequenceMap.values().next().value[0].displaySetIndex + currentLength;
|
||||
const endIndex = beginIndex + (viewportsAmount - currentLength);
|
||||
appendix = displaySets.slice(beginIndex, endIndex);
|
||||
} else {
|
||||
// Get available display sets from the first to the grid size
|
||||
appendix = displaySets.slice(0, viewportsAmount);
|
||||
}
|
||||
|
||||
// Generate the additional data based on the appendix
|
||||
const additionalData = [];
|
||||
appendix.forEach((displaySet, index) => {
|
||||
additionalData.push({
|
||||
viewportIndex: currentViewportIndex + index,
|
||||
studyInstanceUid: displaySet.studyInstanceUid,
|
||||
seriesInstanceUid: displaySet.seriesInstanceUid,
|
||||
displaySetInstanceUid: displaySet.displaySetInstanceUid,
|
||||
sopInstanceUid: displaySet.images[0].sopInstanceUid
|
||||
});
|
||||
});
|
||||
|
||||
// Append the additional data with the viewport data
|
||||
self.viewportData = self.viewportData.concat(additionalData);
|
||||
|
||||
// Push empty objects if the amount is lesser than the grid size
|
||||
while (self.viewportData.length < viewportsAmount) {
|
||||
self.viewportData.push({});
|
||||
}
|
||||
}
|
||||
|
||||
updateViewports() {
|
||||
log.info('updateViewports');
|
||||
|
||||
if (!this.viewportData ||
|
||||
!this.viewportData.length ||
|
||||
this.viewportData.length !== this.getNumberOfViewports()) {
|
||||
this.setDefaultViewportData();
|
||||
}
|
||||
|
||||
// imageViewerViewports occasionally needs relevant layout data in order to set
|
||||
// the element style of the viewport in question
|
||||
var layoutProps = this.layoutProps;
|
||||
var data = $.extend({
|
||||
viewportData: []
|
||||
}, layoutProps);
|
||||
|
||||
this.viewportData.forEach(function(viewportData) {
|
||||
var viewportDataAndLayoutProps = $.extend(viewportData, layoutProps);
|
||||
data.viewportData.push(viewportDataAndLayoutProps);
|
||||
});
|
||||
|
||||
var layoutTemplate = Template[this.layoutTemplateName];
|
||||
|
||||
$(this.parentNode).html('');
|
||||
Blaze.renderWithData(layoutTemplate, data, this.parentNode);
|
||||
|
||||
this.updateSession();
|
||||
|
||||
this.isZoomed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function destroys and re-renders the imageViewerViewport template.
|
||||
* It uses the data provided to load a new display set into the produced viewport.
|
||||
*
|
||||
* @param viewportIndex
|
||||
* @param data
|
||||
*/
|
||||
rerenderViewportWithNewDisplaySet(viewportIndex, data) {
|
||||
// The parent container is identified because it is later removed from the DOM
|
||||
var element = $('.imageViewerViewport').eq(viewportIndex);
|
||||
var container = element.parents('.viewportContainer').get(0);
|
||||
|
||||
// Record the current viewportIndex so this can be passed into the re-rendering call
|
||||
data.viewportIndex = viewportIndex;
|
||||
|
||||
// Update the dictionary of loaded displaySet for the specified viewport
|
||||
this.viewportData[viewportIndex] = {
|
||||
viewportIndex: viewportIndex,
|
||||
displaySetInstanceUid: data.displaySetInstanceUid,
|
||||
seriesInstanceUid: data.seriesInstanceUid,
|
||||
studyInstanceUid: data.studyInstanceUid,
|
||||
renderedCallback: data.renderedCallback,
|
||||
currentImageIdIndex: 0
|
||||
};
|
||||
|
||||
// Remove the hover styling
|
||||
element.find('canvas').not('.magnifyTool').removeClass('faded');
|
||||
|
||||
// Remove the whole template, add in the new one
|
||||
var viewportContainer = element.parents('.removable');
|
||||
|
||||
var newViewportContainer = document.createElement('div');
|
||||
newViewportContainer.className = 'removable';
|
||||
|
||||
// Remove the parent element of the template
|
||||
// This is a workaround since otherwise Blaze UI onDestroyed doesn't fire
|
||||
viewportContainer.remove();
|
||||
|
||||
container.appendChild(newViewportContainer);
|
||||
|
||||
// Render and insert the template
|
||||
Blaze.renderWithData(Template.imageViewerViewport, data, newViewportContainer);
|
||||
|
||||
this.updateSession();
|
||||
}
|
||||
|
||||
enlargeViewport(viewportIndex) {
|
||||
log.info('Zooming Into Viewport: ' + viewportIndex);
|
||||
|
||||
if (!this.viewportData ||
|
||||
!this.viewportData.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clone the array for later
|
||||
this.previousViewportData = this.viewportData.slice(0);
|
||||
|
||||
var singleViewportData = $.extend({}, this.viewportData[viewportIndex]);
|
||||
singleViewportData.rows = 1;
|
||||
singleViewportData.columns = 1;
|
||||
singleViewportData.viewportIndex = 0;
|
||||
|
||||
var data = {
|
||||
viewportData: [singleViewportData],
|
||||
rows: 1,
|
||||
columns: 1
|
||||
};
|
||||
|
||||
var layoutTemplate = Template['gridLayout'];
|
||||
|
||||
$(this.parentNode).html('');
|
||||
Blaze.renderWithData(layoutTemplate, data, this.parentNode);
|
||||
|
||||
this.isZoomed = true;
|
||||
this.zoomedViewportIndex = viewportIndex;
|
||||
this.viewportData = data.viewportData;
|
||||
|
||||
this.updateSession();
|
||||
}
|
||||
|
||||
resetPreviousLayout() {
|
||||
if (!this.isZoomed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.previousViewportData[this.zoomedViewportIndex] = $.extend({}, this.viewportData[0]);
|
||||
this.previousViewportData[this.zoomedViewportIndex].viewportIndex = this.zoomedViewportIndex;
|
||||
this.viewportData = this.previousViewportData;
|
||||
this.updateViewports();
|
||||
}
|
||||
|
||||
toggleEnlargement(viewportIndex) {
|
||||
if (this.isZoomed) {
|
||||
this.resetPreviousLayout();
|
||||
} else {
|
||||
// Don't enlarge the viewport if we only have one Viewport
|
||||
// to begin with
|
||||
if (this.getNumberOfViewports() > 1) {
|
||||
this.enlargeViewport(viewportIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,244 +0,0 @@
|
||||
import {parsingUtils} from './parsingUtils'
|
||||
|
||||
var metaDataLookup = {};
|
||||
|
||||
/**
|
||||
* Cornerstone MetaData provider to store image meta data
|
||||
* Data from instances, series, and studies are associated with
|
||||
* imageIds to facilitate usage of this information by Cornerstone's Tools
|
||||
*
|
||||
* e.g. the imagePlane metaData object contains instance information about
|
||||
* row/column pixel spacing, patient position, and patient orientation. It
|
||||
* is used in CornerstoneTools to position reference lines and orientation markers.
|
||||
*
|
||||
* @param {String} imageId The Cornerstone ImageId
|
||||
* @param {Object} data An object containing instance, series, and study metaData
|
||||
*/
|
||||
addMetaData = function(imageId, data) {
|
||||
var instanceMetaData = data.instance;
|
||||
var seriesMetaData = data.series;
|
||||
var studyMetaData = data.study;
|
||||
var imageIndex = data.imageIndex;
|
||||
var numImages = data.numImages;
|
||||
|
||||
var metaData = {};
|
||||
|
||||
metaData.study = {
|
||||
patientId: studyMetaData.patientId,
|
||||
studyInstanceUid: studyMetaData.studyInstanceUid,
|
||||
studyDate: studyMetaData.studyDate,
|
||||
studyTime: studyMetaData.studyTime,
|
||||
studyDescription: studyMetaData.studyDescription,
|
||||
institutionName: studyMetaData.institutionName
|
||||
};
|
||||
|
||||
metaData.series = {
|
||||
seriesDescription: seriesMetaData.seriesDescription,
|
||||
seriesNumber: seriesMetaData.seriesNumber,
|
||||
modality: seriesMetaData.modality,
|
||||
seriesInstanceUid: seriesMetaData.seriesInstanceUid,
|
||||
numImages: numImages
|
||||
};
|
||||
|
||||
metaData.instance = instanceMetaData;
|
||||
|
||||
metaData.patient = {
|
||||
name: studyMetaData.patientName,
|
||||
id: studyMetaData.patientId,
|
||||
birthDate: studyMetaData.patientBirthDate,
|
||||
sex: studyMetaData.patientSex
|
||||
};
|
||||
|
||||
// If there is sufficient information, populate
|
||||
// the imagePlane object for easier use in the Viewer
|
||||
metaData.imagePlane = getImagePlane(instanceMetaData);
|
||||
|
||||
// Add the metaData to the imageId lookup object
|
||||
metaDataLookup[imageId] = metaData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a set of metaData to the Cornerstone metaData provider given a specific
|
||||
* imageId, type, and dataset
|
||||
*
|
||||
* @param imageId
|
||||
* @param type (e.g. series, instance, tagDisplay)
|
||||
* @param data
|
||||
*/
|
||||
addSpecificMetadata = function(imageId, type, data) {
|
||||
var metaData = {};
|
||||
metaData[type] = data;
|
||||
|
||||
metaDataLookup[imageId] = $.extend(metaDataLookup[imageId], metaData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the related metaData for missing fields given a specified image
|
||||
*
|
||||
* @param image
|
||||
*/
|
||||
updateMetaData = function(image) {
|
||||
var imageMetaData = metaDataLookup[image.imageId];
|
||||
if (!imageMetaData) {
|
||||
return;
|
||||
}
|
||||
|
||||
imageMetaData.instance.sopClassUid = imageMetaData.instance.sopClassUid || image.data.string('x00080016');
|
||||
imageMetaData.instance.sopInstanceUid = imageMetaData.instance.sopInstanceUid || image.data.string('x00080018');
|
||||
|
||||
imageMetaData.instance.rows = imageMetaData.instance.rows || image.rows;
|
||||
imageMetaData.instance.columns = imageMetaData.instance.columns || image.columns;
|
||||
imageMetaData.instance.pixelSpacing = imageMetaData.instance.pixelSpacing || image.data.string('x00280030');
|
||||
imageMetaData.instance.frameOfReferenceUID = imageMetaData.instance.frameOfReferenceUID || image.data.string('x00200052');
|
||||
imageMetaData.instance.imageOrientationPatient = imageMetaData.instance.imageOrientationPatient || image.data.string('x00200037');
|
||||
imageMetaData.instance.imagePositionPatient = imageMetaData.instance.imagePositionPatient || image.data.string('x00200032');
|
||||
|
||||
imageMetaData.instance.sliceThickness = imageMetaData.instance.sliceThickness || image.data.string('x00180050');
|
||||
imageMetaData.instance.sliceLocation = imageMetaData.instance.sliceLocation || image.data.string('x00201041');
|
||||
imageMetaData.instance.tablePosition = imageMetaData.instance.tablePosition || image.data.string('x00189327');
|
||||
imageMetaData.instance.spacingBetweenSlices = imageMetaData.instance.spacingBetweenSlices || image.data.string('x00180088');
|
||||
|
||||
imageMetaData.instance.lossyImageCompression = imageMetaData.instance.lossyImageCompression || image.data.string('x00282110');
|
||||
imageMetaData.instance.lossyImageCompressionRatio = imageMetaData.instance.lossyImageCompressionRatio || image.data.string('x00282112');
|
||||
|
||||
imageMetaData.instance.frameIncrementPointer = imageMetaData.instance.frameIncrementPointer || image.data.string('x00280009');
|
||||
imageMetaData.instance.frameTime = imageMetaData.instance.frameTime || image.data.string('x00181063');
|
||||
imageMetaData.instance.frameTimeVector = imageMetaData.instance.frameTimeVector || image.data.string('x00181065');
|
||||
|
||||
if (!imageMetaData.instance.multiframeMetadata) {
|
||||
imageMetaData.instance.multiframeMetadata = getMultiframeModuleMetaData(image.data);
|
||||
}
|
||||
|
||||
imageMetaData.imagePlane = imageMetaData.imagePlane || getImagePlane(imageMetaData.instance);
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructs and returns the imagePlane given the instance
|
||||
*
|
||||
* @param instance The instance containing information to construct imagePlane
|
||||
* @returns imagePlane The constructed imagePlane to be used in viewer easily
|
||||
*/
|
||||
function getImagePlane(instance) {
|
||||
if (!instance.rows || !instance.columns || !instance.pixelSpacing ||
|
||||
!instance.frameOfReferenceUID || !instance.imageOrientationPatient ||
|
||||
!instance.imagePositionPatient) {
|
||||
return;
|
||||
}
|
||||
|
||||
var imageOrientation = instance.imageOrientationPatient.split('\\');
|
||||
var imagePosition = instance.imagePositionPatient.split('\\');
|
||||
|
||||
var columnPixelSpacing = 1.0;
|
||||
var rowPixelSpacing = 1.0;
|
||||
if (instance.pixelSpacing) {
|
||||
var split = instance.pixelSpacing.split('\\');
|
||||
rowPixelSpacing = parseFloat(split[0]);
|
||||
columnPixelSpacing = parseFloat(split[1]);
|
||||
}
|
||||
|
||||
return {
|
||||
frameOfReferenceUID:
|
||||
instance.frameOfReferenceUID,
|
||||
rows:
|
||||
instance.rows,
|
||||
columns:
|
||||
instance.columns,
|
||||
rowCosines:
|
||||
new cornerstoneMath.Vector3(parseFloat(imageOrientation[0]), parseFloat(imageOrientation[1]), parseFloat(imageOrientation[2])),
|
||||
columnCosines:
|
||||
new cornerstoneMath.Vector3(parseFloat(imageOrientation[3]), parseFloat(imageOrientation[4]), parseFloat(imageOrientation[5])),
|
||||
imagePositionPatient:
|
||||
new cornerstoneMath.Vector3(parseFloat(imagePosition[0]), parseFloat(imagePosition[1]), parseFloat(imagePosition[2])),
|
||||
rowPixelSpacing:
|
||||
rowPixelSpacing,
|
||||
columnPixelSpacing:
|
||||
columnPixelSpacing,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This function extracts miltiframe information from a dicomParser.DataSet object.
|
||||
*
|
||||
* @param dataSet {Object} An instance of dicomParser.DataSet object where multiframe information can be found.
|
||||
* @return {Object} An object containing multiframe image metadata (frameIncrementPointer, frameTime, frameTimeVector, etc).
|
||||
*/
|
||||
getMultiframeModuleMetaData = function(dataSet) {
|
||||
|
||||
var numberOfFrames,
|
||||
frameIncrementPointer,
|
||||
frameTime,
|
||||
frameTimeVector,
|
||||
imageInfo = {
|
||||
isMultiframeImage: false,
|
||||
frameIncrementPointer: null,
|
||||
numberOfFrames: 0,
|
||||
frameTime: 0,
|
||||
frameTimeVector: null,
|
||||
averageFrameRate: 0 // backwards compatibility only... it might be useless in the future
|
||||
};
|
||||
|
||||
if (parsingUtils.isValidDataSet(dataSet)) {
|
||||
|
||||
// (0028,0008) = Number of Frames
|
||||
numberOfFrames = dataSet.intString('x00280008', -1);
|
||||
if (numberOfFrames > 0) {
|
||||
|
||||
// set multi-frame image indicator
|
||||
imageInfo.isMultiframeImage = true;
|
||||
imageInfo.numberOfFrames = numberOfFrames;
|
||||
|
||||
// (0028,0009) = Frame Increment Pointer
|
||||
frameIncrementPointer = parsingUtils.attributeTag(dataSet, 'x00280009') || '';
|
||||
|
||||
if (frameIncrementPointer === 'x00181065') {
|
||||
// Frame Increment Pointer points to Frame Time Vector (0018,1065) field
|
||||
frameTimeVector = parsingUtils.floatArray(dataSet, 'x00181065');
|
||||
if (frameTimeVector instanceof Array && frameTimeVector.length > 0) {
|
||||
imageInfo.frameIncrementPointer = 'frameTimeVector';
|
||||
imageInfo.frameTimeVector = frameTimeVector;
|
||||
frameTime = frameTimeVector.reduce((a, b) => a + b) / frameTimeVector.length;
|
||||
imageInfo.averageFrameRate = 1000 / frameTime;
|
||||
}
|
||||
} else if (frameIncrementPointer === 'x00181063' || frameIncrementPointer === '') {
|
||||
// Frame Increment Pointer points to Frame Time (0018,1063) field or is not defined (for addtional flexibility).
|
||||
// Yet another value is possible for this field (5200,9230 for Multi-frame Functional Groups)
|
||||
// but that case is currently not supported.
|
||||
frameTime = dataSet.floatString('x00181063', -1);
|
||||
if (frameTime > 0) {
|
||||
imageInfo.frameIncrementPointer = 'frameTime';
|
||||
imageInfo.frameTime = frameTime;
|
||||
imageInfo.averageFrameRate = 1000 / frameTime;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return imageInfo;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Looks up metaData for Cornerstone Tools given a specified type and imageId
|
||||
* A type may be, e.g. 'study', or 'patient', or 'imagePlane'. These types
|
||||
* are keys in the stored metaData objects.
|
||||
*
|
||||
* @param type
|
||||
* @param imageId
|
||||
* @returns {Object} Relevant metaData of the specified type
|
||||
*/
|
||||
function provider(type, imageId) {
|
||||
var imageMetaData = metaDataLookup[imageId];
|
||||
if (!imageMetaData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (imageMetaData.hasOwnProperty(type)) {
|
||||
return imageMetaData[type];
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.startup(function() {
|
||||
cornerstoneTools.metaData.addProvider(provider);
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
var resizeTimer;
|
||||
|
||||
handleResize = function() {
|
||||
// Avoid doing DOM manipulation during the resize handler
|
||||
// because it is fired very often.
|
||||
// Resizing is therefore performed 100 ms after the resize event stops.
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(function() {
|
||||
resizeViewportElements();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// Resize viewport elements
|
||||
resizeViewportElements = function() {
|
||||
viewportResizeTimer = setTimeout(function() {
|
||||
var elements = $('.imageViewerViewport').not('.empty');
|
||||
elements.each(function(index, element) {
|
||||
var enabledElement;
|
||||
try {
|
||||
enabledElement = cornerstone.getEnabledElement(element);
|
||||
} catch(error) {
|
||||
return;
|
||||
}
|
||||
|
||||
cornerstone.resize(element, true);
|
||||
|
||||
if (enabledElement.fitToWindow === false) {
|
||||
var imageId = enabledElement.image.imageId;
|
||||
var instance = cornerstoneTools.metaData.get('instance', imageId);
|
||||
var instanceClassViewport = getInstanceClassDefaultViewport(instance, enabledElement, imageId);
|
||||
cornerstone.setViewport(element, instanceClassViewport);
|
||||
}
|
||||
|
||||
// TODO= Refactor this into separate scrollbar resize function
|
||||
var currentOverlay = $(element).siblings('.imageViewerViewportOverlay');
|
||||
var imageControls = currentOverlay.find('.imageControls');
|
||||
currentOverlay.find('.imageControls').height($(element).height());
|
||||
|
||||
// Set it's width to its parent's height
|
||||
// (because webkit is stupid and can't style vertical sliders)
|
||||
var scrollbar = currentOverlay.find('#scrollbar');
|
||||
scrollbar.height(scrollbar.parent().height() - 20);
|
||||
|
||||
var currentImageSlider = currentOverlay.find('#imageSlider');
|
||||
var overlayHeight = currentImageSlider.parent().height();
|
||||
currentImageSlider.width(overlayHeight);
|
||||
});
|
||||
}, 1);
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
setActiveViewport = element => {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewportIndex = $('.imageViewerViewport').index(element);
|
||||
const jQueryElement = $(element);
|
||||
|
||||
// When an ActivateViewport event is fired, update the Meteor Session
|
||||
// with the viewport index that it was fired from.
|
||||
Session.set('activeViewport', viewportIndex);
|
||||
|
||||
// Update the Session variable to the UI re-renders
|
||||
Session.set('LayoutManagerUpdated', Random.id());
|
||||
|
||||
// Add the 'active' class to the parent container to highlight the active viewport
|
||||
$('#imageViewerViewports .viewportContainer').removeClass('active');
|
||||
jQueryElement.parents('.viewportContainer').addClass('active');
|
||||
|
||||
// Finally, enable stack prefetching and hide the reference lines from
|
||||
// the newly activated viewport that has a canvas
|
||||
|
||||
if (jQueryElement.find('canvas').length) {
|
||||
// Cornerstone Tools compare DOM elements (check getEnabledElement cornerstone function)
|
||||
// so we can't pass a jQuery object as an argument, otherwise it throws an excepetion
|
||||
const domElement = jQueryElement.get(0);
|
||||
enablePrefetchOnElement(domElement);
|
||||
displayReferenceLines(domElement);
|
||||
}
|
||||
|
||||
// Set the div to focused, so keypress events are handled
|
||||
//$(element).focus();
|
||||
//.focus() event breaks in FF&IE
|
||||
jQueryElement.triggerHandler('focus');
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* Restores the browser focus to the currently specified active viewport
|
||||
* as determined from Meteor's Session variable.
|
||||
*
|
||||
* This is allows keydown events to be captured on the focused element
|
||||
*/
|
||||
setFocusToActiveViewport = function() {
|
||||
// Get the list of viewports
|
||||
var viewports = $('.imageViewerViewport');
|
||||
|
||||
// Get the current active viewport index from Session
|
||||
var activeViewportIndex = Session.get('activeViewport');
|
||||
|
||||
// Find the div from the list of viewports
|
||||
var activeViewport = viewports.eq(activeViewportIndex);
|
||||
|
||||
// Set the browser focus to this div
|
||||
activeViewport.focus();
|
||||
};
|
||||
Loaded 100 of 537 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user