Work on refactoring measurements out of Lesion Tracker package

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

No files matched your search

-198
View File
@@ -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">&times;</span>
</button>
<h4 class="modal-title"
id="aboutModalLabel">
About
</h4>
</div>
<div class="modal-body">
<a target='_blank'
class='forkOnGithub'
href="{{githubUrl}}">
<img src="https://camo.githubusercontent.com/365986a132ccd6a44c23a9169022c0b5c890c387/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f7265645f6161303030302e706e67"
alt="Fork me on GitHub"
data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_red_aa0000.png">
</a>
<div class="logo p-x-3 p-y-3">
<svg class="logoImage">
<use xlink:href="/packages/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
@@ -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>
@@ -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();
}
}
});
@@ -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
@@ -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>
@@ -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
@@ -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>
@@ -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));
}
});
}
});
@@ -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();
}
});
@@ -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>
@@ -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);
}
});
@@ -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')
@@ -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>
@@ -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);
}
});
@@ -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
@@ -1,8 +0,0 @@
<template name="viewportOrientationMarkers">
<div class="viewportOrientationMarkers noselect">
<div class="topMid orientationMarker">
</div>
<div class="leftMid orientationMarker">
</div>
</div>
</template>
@@ -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);
}
};
@@ -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
-28
View File
@@ -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);
};
-123
View File
@@ -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);
}
});
};
-309
View File
@@ -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);
}
};
-27
View File
@@ -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 '';
});
-3
View File
@@ -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;
};
-59
View File
@@ -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;
};
-234
View File
@@ -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);
}
}
}
};
-244
View File
@@ -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