diff --git a/LesionTracker/client/components/toolbarSection/toolbarSection.js b/LesionTracker/client/components/toolbarSection/toolbarSection.js
index 18cb39450..7c0d80dac 100644
--- a/LesionTracker/client/components/toolbarSection/toolbarSection.js
+++ b/LesionTracker/client/components/toolbarSection/toolbarSection.js
@@ -170,6 +170,14 @@ Template.toolbarSection.helpers({
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-elliptical-roi'
});
+ buttonData.push({
+ id: 'linkStackScroll',
+ title: 'Link Scroll',
+ classes: 'imageViewerCommand toolbarSectionButton nonAutoDisableState',
+ svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-link-stack-scroll',
+ disableFunction: isStackScrollLinkingDisabled
+ });
+
buttonData.push({
id: 'toggleCineDialog',
title: 'CINE',
@@ -235,7 +243,7 @@ Template.toolbarSection.onRendered(function() {
// Set disabled/enabled tool buttons that are set in toolManager
const states = toolManager.getToolDefaultStates();
const disabledToolButtons = states.disabledToolButtons;
- const allToolbarButtons = $('.toolbarSection').find('.toolbarSectionButton');
+ const allToolbarButtons = $('.toolbarSection').find('.toolbarSectionButton:not(.nonAutoDisableState)');
// Additional toolbar buttons whose classes are not toolbarSectionButton
allToolbarButtons.push($('#toolbarSectionEntry')[0]);
diff --git a/LesionTracker/client/components/viewer/viewer.js b/LesionTracker/client/components/viewer/viewer.js
index 0a5ffbd85..c46122b95 100644
--- a/LesionTracker/client/components/viewer/viewer.js
+++ b/LesionTracker/client/components/viewer/viewer.js
@@ -39,7 +39,8 @@ Template.viewer.onCreated(() => {
flipV,
flipH,
rotateL,
- rotateR
+ rotateR,
+ linkStackScroll
});
if (ViewerData[contentId].loadedSeriesData) {
diff --git a/Packages/ohif-viewerbase/assets/icons.svg b/Packages/ohif-viewerbase/assets/icons.svg
index 36b40d99a..c6885d1ad 100644
--- a/Packages/ohif-viewerbase/assets/icons.svg
+++ b/Packages/ohif-viewerbase/assets/icons.svg
@@ -235,4 +235,13 @@
+
+
+ Stack Scroll Sync
+
+
+
+
+
+
diff --git a/Packages/ohif-viewerbase/client/components/viewer/toolbarSectionButton/toolbarSectionButton.html b/Packages/ohif-viewerbase/client/components/viewer/toolbarSectionButton/toolbarSectionButton.html
index 950fe223b..46f12aca8 100644
--- a/Packages/ohif-viewerbase/client/components/viewer/toolbarSectionButton/toolbarSectionButton.html
+++ b/Packages/ohif-viewerbase/client/components/viewer/toolbarSectionButton/toolbarSectionButton.html
@@ -3,7 +3,7 @@
{{>UI.dynamic template=this.buttonTemplateName data=this}}
{{else}}
{{#if this.svgLink}}
diff --git a/Packages/ohif-viewerbase/client/components/viewer/toolbarSectionButton/toolbarSectionButton.js b/Packages/ohif-viewerbase/client/components/viewer/toolbarSectionButton/toolbarSectionButton.js
index 5cf7283aa..786a84eda 100644
--- a/Packages/ohif-viewerbase/client/components/viewer/toolbarSectionButton/toolbarSectionButton.js
+++ b/Packages/ohif-viewerbase/client/components/viewer/toolbarSectionButton/toolbarSectionButton.js
@@ -12,7 +12,8 @@ Template.toolbarSectionButton.helpers({
}
},
disableButton() {
- return this.disableFunction();
+ const instance = Template.instance();
+ return this.disableFunction && this.disableFunction();
}
});
diff --git a/Packages/ohif-viewerbase/client/lib/setActiveViewport.js b/Packages/ohif-viewerbase/client/lib/setActiveViewport.js
index 0b2dfdadd..fa6ac2774 100644
--- a/Packages/ohif-viewerbase/client/lib/setActiveViewport.js
+++ b/Packages/ohif-viewerbase/client/lib/setActiveViewport.js
@@ -1,3 +1,5 @@
+import { OHIF } from 'meteor/ohif:core';
+
setActiveViewport = element => {
if (!element) {
return;
@@ -26,6 +28,7 @@ setActiveViewport = element => {
const domElement = jQueryElement.get(0);
enablePrefetchOnElement(domElement);
displayReferenceLines(domElement);
+ OHIF.viewer.stackImagePositionOffsetSynchronizer.update();
}
// Set the div to focused, so keypress events are handled
diff --git a/Packages/ohif-viewerbase/client/lib/stackImagePositionOffsetSynchronizer.js b/Packages/ohif-viewerbase/client/lib/stackImagePositionOffsetSynchronizer.js
new file mode 100644
index 000000000..6e046d19a
--- /dev/null
+++ b/Packages/ohif-viewerbase/client/lib/stackImagePositionOffsetSynchronizer.js
@@ -0,0 +1,135 @@
+import { OHIF } from 'meteor/ohif:core';
+import { Session } from 'meteor/session';
+import { $ } from 'meteor/jquery';
+
+class StackImagePositionOffsetSynchronizer {
+ constructor() {
+ this.active = false;
+ this.syncedViewportElements = [];
+ this.synchronizer = new cornerstoneTools.Synchronizer("CornerstoneNewImage", cornerstoneTools.stackImagePositionOffsetSynchronizer)
+ }
+
+ static get ELEMENT_DISABLED_EVENT() {
+ return 'CornerstoneElementDisabled.StackImagePositionOffsetSynchronizer';
+ }
+
+ isActive() {
+ return this.active;
+ }
+
+ activate() {
+ if(this.isActive()) {
+ return;
+ }
+
+ const viewportElements = this.getLinkableViewports();
+
+ viewportElements.forEach((viewportElement, index) => {
+ this.synchronizer.add(viewportElement);
+ this.syncedViewportElements.push(viewportElement);
+
+ $(viewportElement).on(StackImagePositionOffsetSynchronizer.ELEMENT_DISABLED_EVENT, this.elementDisabledHandler(this));
+ });
+
+ this.active = true;
+ }
+
+ deactivate() {
+ if(!this.isActive()) {
+ return;
+ }
+
+ while(this.syncedViewportElements.length) {
+ const viewportElement = this.syncedViewportElements[0];
+ this.removeViewportElement(viewportElement);
+ }
+
+ this.active = false;
+ }
+
+ update() {
+ if(!this.isActive()) {
+ return;
+ }
+
+ const activeViewportElement = this.getActiveViewportElement();
+
+ if(this.isViewportSynced(activeViewportElement)) {
+ return;
+ }
+
+ this.deactivate();
+ this.activate();
+ }
+
+ getActiveViewportElement() {
+ const viewportIndex = Session.get('activeViewport') || 0;
+ return $('.imageViewerViewport').get(viewportIndex);
+ }
+
+ isViewportSynced(viewportElement) {
+ let isSynced = false;
+
+ this.syncedViewportElements.forEach(syncedViewportElement => {
+ isSynced = isSynced || (syncedViewportElement === viewportElement);
+ });
+
+ return isSynced;
+ }
+
+ removeViewportElement(viewportElement) {
+ const index = this.syncedViewportElements.indexOf(viewportElement);
+
+ if(index === -1) {
+ return;
+ }
+
+ this.syncedViewportElements.splice(index, 1);
+ this.synchronizer.remove(viewportElement);
+ $(viewportElement).off(StackImagePositionOffsetSynchronizer.ELEMENT_DISABLED_EVENT);
+ }
+
+ elementDisabledHandler(context) {
+ return (e, eventData) => {
+ context.removeViewportElement(eventData.element);
+ }
+ }
+
+ getLinkableViewports() {
+ const activeViewportElement = this.getActiveViewportElement();
+ const activeViewportImageNormal = this.getViewportImageNormal(activeViewportElement);
+ const viewportElements = [];
+
+ $('.imageViewerViewport').each((index, viewportElement) => {
+ const viewportImageNormal = this.getViewportImageNormal(viewportElement);
+
+ if(activeViewportImageNormal && viewportImageNormal) {
+ const angleInRadians = viewportImageNormal.angleTo(activeViewportImageNormal);
+
+ // Pi / 12 radians = 15 degrees
+ // If the angle between two vectors is Pi, it means they are just inverted
+ if (angleInRadians < Math.PI / 12 || angleInRadians === Math.PI) {
+ viewportElements.push(viewportElement)
+ }
+ }
+ });
+
+ return viewportElements;
+ }
+
+ getViewportImageNormal(element) {
+ element = $(element).get(0);
+
+ try {
+ const enabledElement = cornerstone.getEnabledElement(element);
+ const imageId = enabledElement.image.imageId;
+ const imagePlane = cornerstoneTools.metaData.get('imagePlane', imageId);
+
+ return imagePlane.rowCosines.clone().cross(imagePlane.columnCosines);;
+ } catch(error) {
+ console.log(error.message);
+ }
+ }
+}
+
+OHIF.viewer.stackImagePositionOffsetSynchronizer = new StackImagePositionOffsetSynchronizer();
\ No newline at end of file
diff --git a/Packages/ohif-viewerbase/client/lib/viewportFunctions.js b/Packages/ohif-viewerbase/client/lib/viewportFunctions.js
index 961f11b6b..1c9928b9b 100644
--- a/Packages/ohif-viewerbase/client/lib/viewportFunctions.js
+++ b/Packages/ohif-viewerbase/client/lib/viewportFunctions.js
@@ -115,6 +115,16 @@ clearTools = () => {
cornerstone.updateImage(element);
};
+linkStackScroll = () => {
+ const synchronizer = OHIF.viewer.stackImagePositionOffsetSynchronizer;
+
+ if(synchronizer.isActive()) {
+ synchronizer.deactivate();
+ } else {
+ synchronizer.activate();
+ }
+}
+
// Toggle the play/stop state for the cornerstone clip tool
toggleCinePlay = () => {
// Get the active viewport element
@@ -168,6 +178,8 @@ isPlaying = () => {
return false;
};
+
+
// Check if a study has multiple frames
hasMultipleFrames = () => {
// Its called everytime active viewport and/or layout change
@@ -239,5 +251,16 @@ hasMultipleFrames = () => {
return false;
};
+isStackScrollLinkingDisabled = () => {
+ // Its called everytime active viewport and/or layout change
+ Session.get('activeViewport');
+ Session.get('LayoutManagerUpdated');
+
+ const synchronizer = OHIF.viewer.stackImagePositionOffsetSynchronizer;
+ const linkableViewports = synchronizer.getLinkableViewports();
+
+ return linkableViewports.length <= 1;
+}
+
// Create an event listener to update playing state when a clip stops playing
$(window).on('CornerstoneToolsClipStopped', () => Session.set('UpdateCINE', Random.id()));
diff --git a/Packages/ohif-viewerbase/package.js b/Packages/ohif-viewerbase/package.js
index 5c4eefdcb..1c7c5c162 100644
--- a/Packages/ohif-viewerbase/package.js
+++ b/Packages/ohif-viewerbase/package.js
@@ -218,6 +218,7 @@ Package.onUse(function(api) {
api.addFiles('client/lib/resizeViewportElements.js', 'client');
api.addFiles('client/lib/setFocusToActiveViewport.js', 'client');
api.addFiles('client/lib/updateAllViewports.js', 'client');
+ api.addFiles('client/lib/stackImagePositionOffsetSynchronizer.js', 'client');
api.addFiles('client/lib/instanceClassSpecificViewport.js', 'client');
api.addFiles('client/lib/setMammogramViewportAlignment.js', 'client');
@@ -252,6 +253,7 @@ Package.onUse(function(api) {
api.export('sopClassDictionary', 'client');
api.export('addMetaData', 'client');
api.export('hasMultipleFrames', 'client');
+ api.export('isStackScrollLinkingDisabled')
// Viewer management objects
api.export('toolManager', 'client');
@@ -295,4 +297,5 @@ Package.onUse(function(api) {
api.export('flipH', 'client');
api.export('rotateR', 'client');
api.export('rotateL', 'client');
+ api.export('linkStackScroll', 'client');
});