From 4721400b3428685ae1d344cf0fd9a7a16274262d Mon Sep 17 00:00:00 2001 From: Erik Ziegler Date: Fri, 23 Oct 2015 14:08:51 +0200 Subject: [PATCH] Updates to save viewport data across sessions / while switching tabs --- .gitignore | 1 + LesionTracker/client/body.styl | 5 + .../client/components/layout/layout.styl | 2 +- LesionTracker/client/components/viewer.js | 75 +++++---- LesionTracker/client/route.js | 26 +++- OHIFViewer/client/components/viewer/viewer.js | 81 +++++----- OHIFViewer/client/routes.js | 26 ++-- .../cornerstone/client/hangingProtocol.js | 45 ++++++ .../cornerstone/client/measurementManager.js | 77 ++++++++++ .../client/measurementManagerExample.js | 57 +++++++ Packages/cornerstone/package.js | 7 + .../components/basic/layout/layout.html | 2 +- .../components/basic/layout/layout.styl | 8 + .../imageViewerViewport.js | 142 +++++++++++------- .../imageViewerViewports.html | 2 +- .../imageViewerViewports.js | 113 ++++++++------ .../viewer/layoutChooser/layoutChooser.js | 4 +- .../viewer/playClipButton/playClipButton.js | 2 +- .../lib/rerenderViewportWithNewSeries.js | 6 +- .../worklist/components/tabTitle/tabTitle.js | 4 +- Packages/worklist/components/worklist.html | 2 +- Packages/worklist/components/worklist.js | 41 +++-- Packages/worklist/components/worklist.styl | 8 + Packages/worklist/package.js | 2 + etc/nodeCORSProxy.example | 20 +++ etc/nodeCORSProxy.js | 20 +++ etc/setupLocalOrthanc.sh | 11 ++ 27 files changed, 585 insertions(+), 204 deletions(-) create mode 100644 LesionTracker/client/body.styl create mode 100644 Packages/cornerstone/client/hangingProtocol.js create mode 100644 Packages/cornerstone/client/measurementManager.js create mode 100644 Packages/cornerstone/client/measurementManagerExample.js create mode 100644 etc/nodeCORSProxy.example create mode 100644 etc/nodeCORSProxy.js create mode 100755 etc/setupLocalOrthanc.sh diff --git a/.gitignore b/.gitignore index 0167b87cc..b0746064f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .meteor/local .meteor/meteorite +node_modules \ No newline at end of file diff --git a/LesionTracker/client/body.styl b/LesionTracker/client/body.styl new file mode 100644 index 000000000..498ae65e3 --- /dev/null +++ b/LesionTracker/client/body.styl @@ -0,0 +1,5 @@ +body + background-color: #202020 + +#search-panel-head + background-color: #8845AB diff --git a/LesionTracker/client/components/layout/layout.styl b/LesionTracker/client/components/layout/layout.styl index dbe02362d..7a82d278b 100644 --- a/LesionTracker/client/components/layout/layout.styl +++ b/LesionTracker/client/components/layout/layout.styl @@ -7,7 +7,7 @@ font-family: "Sanchez" color: #C1C1C1 -.navbar-form, .navbar-default .navbar-nav > li > a +.navbar-default .navbar-nav > li > a color: #C1C1C1 font-weight: bold font-size: 12pt diff --git a/LesionTracker/client/components/viewer.js b/LesionTracker/client/components/viewer.js index 8e69c9f3f..f72198aa7 100644 --- a/LesionTracker/client/components/viewer.js +++ b/LesionTracker/client/components/viewer.js @@ -1,16 +1,30 @@ -OHIF = { - viewer: {} -}; +function resizeViewports() { + // Handle resizing of image viewer viewports + // For some reason, this seems to need to be on + // another delay, or the resizing won't work properly + viewportResizeTimer = setTimeout(function() { + var elements = $('.imageViewerViewport'); + elements.each(function(index) { + var element = this; + if (!element) { + return; + } + cornerstone.resize(element, true); + }); + }, 1); +} -OHIF.viewer.imageViewerLoadedSeriesDictionary = {}; -OHIF.viewer.imageViewerCurrentImageIdIndexDictionary = {}; -OHIF.viewer.loadIndicatorDelay = 3000; -OHIF.viewer.defaultTool = 'wwwc'; -OHIF.viewer.refLinesEnabled = true; -OHIF.viewer.isPlaying = {}; +Template.viewer.onCreated(function() { + console.log("Image Viewer onCreated"); + OHIF = { + viewer: {} + }; + + OHIF.viewer.loadIndicatorDelay = 3000; + OHIF.viewer.defaultTool = 'wwwc'; + OHIF.viewer.refLinesEnabled = true; + OHIF.viewer.isPlaying = {}; -Meteor.startup(function() { - OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer("CornerstoneNewImage", cornerstoneTools.updateImageSynchronizer); OHIF.viewer.functionList = { invert: function(element) { var viewport = cornerstone.getViewport(element); @@ -40,25 +54,7 @@ Meteor.startup(function() { trigger: 'hover' }; } -}); -function resizeViewports() { - // Handle resizing of image viewer viewports - // For some reason, this seems to need to be on - // another delay, or the resizing won't work properly - viewportResizeTimer = setTimeout(function() { - var elements = $('.imageViewerViewport'); - elements.each(function(index) { - var element = this; - if (!element) { - return; - } - cornerstone.resize(element, true); - }); - }, 1); -} - -Template.viewer.onCreated(function() { if (this.data.activeViewport === undefined) { this.data.activeViewport = new ReactiveVar(0); } @@ -68,6 +64,27 @@ Template.viewer.onCreated(function() { if (this.data.viewportColumns === undefined) { this.data.viewportColumns = new ReactiveVar(2); } + + var contentId = this.data.contentId; + + // Update the viewer data object + ViewerData[contentId].viewportColumns = this.data.viewportColumns; + ViewerData[contentId].viewportRows = this.data.viewportRows; + ViewerData[contentId].activeViewport = this.data.activeViewport; + Session.set('ViewerData', ViewerData); + + if (ViewerData[contentId].viewer) { + OHIF.viewer = ViewerData[contentId].viewer; + } else { + OHIF.viewer.imageViewerLoadedSeriesDictionary = {}; + ViewerData[contentId].viewer = OHIF.viewer; + } + + OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer("CornerstoneNewImage", cornerstoneTools.updateImageSynchronizer); +}); + +Template.viewer.onDestroyed(function() { + OHIF.viewer.updateImageSynchronizer.destroy(); }); // Avoid doing DOM manipulation during the resize handler diff --git a/LesionTracker/client/route.js b/LesionTracker/client/route.js index ab2b4be0d..2fa8c2777 100644 --- a/LesionTracker/client/route.js +++ b/LesionTracker/client/route.js @@ -9,9 +9,21 @@ Router.route('/', function () { }); tabs = new Meteor.Collection(null); +Session.setDefault('ViewerData', {}); +Session.setDefault('ViewerDataUpdated', Random.id()); + +Object.keys(ViewerData).forEach(function(contentId) { + var tabData = ViewerData[contentId]; + var data = { + title: tabData.title, + contentid: tabData.contentId, + }; + tabs.insert(data); +}); + Router.route('/viewer/:_id', { - layoutTemplate: 'layoutLesionTracker', + layoutTemplate: 'layout', name: 'viewer', onBeforeAction: function() { var self = this; @@ -19,21 +31,19 @@ Router.route('/viewer/:_id', { Meteor.call('GetStudyMetadata', this.params._id, function(error, study) { sortStudy(study); - var data = { - studies: [study] - }; var title = study.seriesList[0].instances[0].patientName; var contentid = generateUUID(); - var newTabObject = { + var data = { title: title, contentid: contentid, }; - tabs.insert(newTabObject); + tabs.insert(data); + + data.studies = [study]; self.render('worklist'); - Session.set('DataInTab#' + contentid, data); - Session.set('OpenNewTabEvent', contentid); + Session.set('OpenNewTabEvent', data); }); } }); \ No newline at end of file diff --git a/OHIFViewer/client/components/viewer/viewer.js b/OHIFViewer/client/components/viewer/viewer.js index b5c0ff60d..ae0c0b139 100644 --- a/OHIFViewer/client/components/viewer/viewer.js +++ b/OHIFViewer/client/components/viewer/viewer.js @@ -1,16 +1,30 @@ -OHIF = { - viewer: {} -}; +function resizeViewports() { + // Handle resizing of image viewer viewports + // For some reason, this seems to need to be on + // another delay, or the resizing won't work properly + viewportResizeTimer = setTimeout(function() { + var elements = $('.imageViewerViewport'); + elements.each(function(index) { + var element = this; + if (!element) { + return; + } + cornerstone.resize(element, true); + }); + }, 1); +} -OHIF.viewer.imageViewerLoadedSeriesDictionary = {}; -OHIF.viewer.imageViewerCurrentImageIdIndexDictionary = {}; -OHIF.viewer.loadIndicatorDelay = 3000; -OHIF.viewer.defaultTool = 'wwwc'; -OHIF.viewer.refLinesEnabled = true; -OHIF.viewer.isPlaying = {}; +Template.viewer.onCreated(function() { + console.log("Image Viewer onCreated"); + OHIF = { + viewer: {} + }; + + OHIF.viewer.loadIndicatorDelay = 3000; + OHIF.viewer.defaultTool = 'wwwc'; + OHIF.viewer.refLinesEnabled = true; + OHIF.viewer.isPlaying = {}; -Meteor.startup(function() { - OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer("CornerstoneNewImage", cornerstoneTools.updateImageSynchronizer); OHIF.viewer.functionList = { invert: function(element) { var viewport = cornerstone.getViewport(element); @@ -40,25 +54,7 @@ Meteor.startup(function() { trigger: 'hover' }; } -}); -function resizeViewports() { - // Handle resizing of image viewer viewports - // For some reason, this seems to need to be on - // another delay, or the resizing won't work properly - viewportResizeTimer = setTimeout(function() { - var elements = $('.imageViewerViewport'); - elements.each(function(index) { - var element = this; - if (!element) { - return; - } - cornerstone.resize(element, true); - }); - }, 1); -} - -Template.viewer.onCreated(function() { if (this.data.activeViewport === undefined) { this.data.activeViewport = new ReactiveVar(0); } @@ -69,13 +65,26 @@ Template.viewer.onCreated(function() { this.data.viewportColumns = new ReactiveVar(1); } - var viewportColumns = this.data.viewportColumns.get(); - var viewportRows = this.data.viewportRows.get(); - var activeViewport = this.data.activeViewport.get(); - console.log('Reactive changes!'); - console.log('viewportColumns: ' + viewportColumns); - console.log('viewportRows: ' + viewportRows); - console.log('activeViewport: ' + activeViewport); + var contentId = this.data.contentId; + + // Update the viewer data object + ViewerData[contentId].viewportColumns = this.data.viewportColumns; + ViewerData[contentId].viewportRows = this.data.viewportRows; + ViewerData[contentId].activeViewport = this.data.activeViewport; + Session.set('ViewerData', ViewerData); + + if (ViewerData[contentId].viewer) { + OHIF.viewer = ViewerData[contentId].viewer; + } else { + OHIF.viewer.imageViewerLoadedSeriesDictionary = {}; + ViewerData[contentId].viewer = OHIF.viewer; + } + + OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer("CornerstoneNewImage", cornerstoneTools.updateImageSynchronizer); +}); + +Template.viewer.onDestroyed(function() { + OHIF.viewer.updateImageSynchronizer.destroy(); }); // Avoid doing DOM manipulation during the resize handler diff --git a/OHIFViewer/client/routes.js b/OHIFViewer/client/routes.js index e7681124e..84ae25bfe 100644 --- a/OHIFViewer/client/routes.js +++ b/OHIFViewer/client/routes.js @@ -4,13 +4,23 @@ Router.configure({ notFoundTemplate: 'notFound' }); - - Router.route('/', function () { this.render('worklist', {}); }); tabs = new Meteor.Collection(null); +Session.setDefault('ViewerData', {}); +Session.setDefault('ViewerDataUpdated', Random.id()); + +Object.keys(ViewerData).forEach(function(contentId) { + var tabData = ViewerData[contentId]; + var data = { + title: tabData.title, + contentid: tabData.contentId, + }; + tabs.insert(data); +}); + Router.route('/viewer/:_id', { layoutTemplate: 'layout', @@ -21,21 +31,19 @@ Router.route('/viewer/:_id', { Meteor.call('GetStudyMetadata', this.params._id, function(error, study) { sortStudy(study); - var data = { - studies: [study] - }; var title = study.seriesList[0].instances[0].patientName; var contentid = generateUUID(); - var newTabObject = { + var data = { title: title, contentid: contentid, }; - tabs.insert(newTabObject); + tabs.insert(data); + + data.studies = [study]; self.render('worklist'); - Session.set('DataInTab#' + contentid, data); - Session.set('OpenNewTabEvent', contentid); + Session.set('OpenNewTabEvent', data); }); } }); \ No newline at end of file diff --git a/Packages/cornerstone/client/hangingProtocol.js b/Packages/cornerstone/client/hangingProtocol.js new file mode 100644 index 000000000..591481964 --- /dev/null +++ b/Packages/cornerstone/client/hangingProtocol.js @@ -0,0 +1,45 @@ +var hangingProtocol; + +function defaultHangingProtocol(inputData) { + var studies = inputData.studies; + var viewportRows = inputData.viewportRows; + var viewportColumns = inputData.viewportColumns; + + // This is the most basic hanging protocol. + var stacks = []; + studies.forEach(function(study) { + study.seriesList.forEach(function(series) { + var stack = { + series: series, + study: study + }; + stacks.push(stack); + }); + }); + + var viewportData = []; + + var numViewports = viewportRows * viewportColumns; + for (var i=0; i < numViewports; ++i) { + if (i >= stacks.length) { + // We don't have enough stacks to fill the desired number of viewports, so stop here + break; + } + viewportData[i] = { + seriesInstanceUid: stacks[i].series.seriesInstanceUid, + studyInstanceUid: stacks[i].study.studyInstanceUid, + currentImageIdIndex: 0 + }; + } + return viewportData; +} + +getHangingProtocol = function() { + return hangingProtocol; +}; + +setHangingProtocol = function(protocol) { + hangingProtocol = protocol; +}; + +setHangingProtocol(defaultHangingProtocol); diff --git a/Packages/cornerstone/client/measurementManager.js b/Packages/cornerstone/client/measurementManager.js new file mode 100644 index 000000000..961bcc9bf --- /dev/null +++ b/Packages/cornerstone/client/measurementManager.js @@ -0,0 +1,77 @@ +(function($, cornerstoneTools) { + + 'use strict'; + + // This object manages a collection of measurements + function MeasurementManager() { + // Define some basic validator functions (everything is always valid) + this.singleMeasurementValidator = function() { + return {isValid: true}; + }; + + this.measurementSetValidator = function() { + return {isValid: true}; + }; + + var that = this; + that.measurements = []; + + // adds an element as both a source and a target + this.add = function(measurement, validateSingle, validateSet) { + var index = that.measurements.push(measurement); + + if (validateSingle === true) { + measurementValid = singleMeasurementValidator(measurement); + } + + if (validateSet === true) { + measurementSetValid = measurementSetValidator(measurement); + } + + // fire event + var eventDetail = { + index: index, + measurement: measurement, + measurementValid: measurementValid, + measurementSetValid: measurementSetValid + }; + + $(that).trigger('CornerstoneMeasurementAdded', eventDetail); + }; + + this.remove = function(index) { + var measurement = that.measurements[index]; + that.measurements.splice(index, 1); + // fire event + var eventDetail = { + index: index, + measurement: measurement + }; + $(that).trigger('CornerstoneMeasurementRemoved', eventDetail); + }; + + // Add set/get measurementValidator to run on each single measurement + // (e.g. length > 10 mm, location must be 30 mm from lymph node) + this.setSingleMeasurementValidator = function(validator) { + this.singleMeasurementValidator = validator; + }; + + this.getSingleMeasurementValidator = function(validator) { + return singleMeasurementValidator; + }; + + // Add set/get groupValidator to run on all of the measurements together + // (e.g. the number of measurments must be <= 2) + this.setMeasurementSetValidator = function(validator) { + this.measurementSetValidator = validator; + }; + + this.getMeasurementSetValidator = function(validator) { + return measurementSetValidator; + }; + } + + // module/private exports + cornerstoneTools.MeasurementManager = new MeasurementManager(); + +})($, cornerstoneTools); \ No newline at end of file diff --git a/Packages/cornerstone/client/measurementManagerExample.js b/Packages/cornerstone/client/measurementManagerExample.js new file mode 100644 index 000000000..824437ec9 --- /dev/null +++ b/Packages/cornerstone/client/measurementManagerExample.js @@ -0,0 +1,57 @@ +// Example usage for something like RECIST +var tumourLengthMeasurementManager = cornerstoneTools.MeasurementManager; +function singleValidator(measurement) { + var result = { + isValid: true + }; + if (measurement.distance < 10) { + result.isValid = false; + result.details = 'Measurement distance must be less than 10 mm'; + } + return result; +} + +function setValidator(measurements) { + var result = { + isValid: true + }; + if (measurements.length > 2) { + result.isValid = false; + result.details = 'The number of measurements per image must be less than 2'; + } + return result; +} + +// Or a remote call +function remoteSetValidator(measurements) { + var deferred = $.Deferred; + + var result = { + isValid: true, + promise: deferred.promise() + }; + + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + if (xhr.status == 200) { + var response = JSON.parse(xhr.response); + if (response.isValid) { + deferred.resolve(xhr.responseText); + } else { + deferred.reject(xhr.responseText); + } + } else { + // The request didn't succeed + deferred.reject(xhr.responseText); + } + } + }; + xhr.open("POST", "/urlToValidateRECIST", true); + xhr.send(measurements); + + return result; +} + +tumourLengthMeasurementManager.setSingleMeasurementValidator(singleValidator); +tumourLengthMeasurementManager.setMeasurementSetValidator(setValidator); diff --git a/Packages/cornerstone/package.js b/Packages/cornerstone/package.js index 645fed6a3..99a454d8d 100644 --- a/Packages/cornerstone/package.js +++ b/Packages/cornerstone/package.js @@ -17,11 +17,18 @@ Package.onUse(function (api) { api.addFiles('client/dicomParser.js', 'client', {bare: true}); api.addFiles('client/hammer.js', 'client', {bare: true}); + api.addFiles('client/hangingProtocol.js', 'client', {bare: true}); + api.addFiles('client/measurementManager.js', 'client', {bare: true}); + api.addFiles('client/measurementManagerExample.js', 'client', {bare: true}); + api.export("cornerstone", 'client'); api.export("cornerstoneMath", 'client'); api.export("cornerstoneTools", 'client'); api.export("cornerstoneWADOImageLoader", 'client'); api.export("cornerstoneWADORSImageLoader", 'client'); api.export("dicomParser", 'client'); + + api.export("getHangingProtocol", 'client'); + api.export("setHangingProtocol", 'client'); }); diff --git a/Packages/viewerbase/components/basic/layout/layout.html b/Packages/viewerbase/components/basic/layout/layout.html index e7eb311eb..fc3a7c5e3 100644 --- a/Packages/viewerbase/components/basic/layout/layout.html +++ b/Packages/viewerbase/components/basic/layout/layout.html @@ -1,5 +1,5 @@