diff --git a/LesionTracker/client/components/viewer.js b/LesionTracker/client/components/viewer.js index 712a3188a..75a253db4 100644 --- a/LesionTracker/client/components/viewer.js +++ b/LesionTracker/client/components/viewer.js @@ -115,6 +115,7 @@ Template.viewer.onCreated(function() { self.subscribe('singlePatientAssociatedStudies', dataContext.studies[0].patientId); self.subscribe('singlePatientTimepoints', dataContext.studies[0].patientId); self.subscribe('singlePatientMeasurements', dataContext.studies[0].patientId); + self.subscribe('singlePatientImageMeasurements', dataContext.studies[0].patientId); var subscriptionsReady = self.subscriptionsReady(); log.info('autorun viewer.js. Ready: ' + subscriptionsReady); @@ -145,6 +146,39 @@ Template.viewer.onCreated(function() { } }); + ImageMeasurements.find().observe({ + added: function(data) { + if (data.clientId === ClientId) { + return; + } + + syncImageMeasurementAndToolData(data); + + // Update each displayed viewport + updateAllViewports(); + }, + changed: function(data) { + if (data.clientId === ClientId) { + return; + } + + syncImageMeasurementAndToolData(data); + + // Update each displayed viewport + updateAllViewports(); + }, + removed: function(data) { + if (data.clientId === ClientId) { + return; + } + + removeToolDataWithMeasurementId(data.imageId, data.toolType, data.id); + + // Update each displayed viewport + updateAllViewports(); + } + }); + Measurements.find().observe({ added: function(data) { if (data.clientId === ClientId) { @@ -170,10 +204,7 @@ Template.viewer.onCreated(function() { } // Update each displayed viewport - var viewports = $('.imageViewerViewport').not('.empty'); - viewports.each(function(index, element) { - cornerstone.updateImage(element); - }); + updateAllViewports(); }, changed: function(data) { if (data.clientId === ClientId) { @@ -188,10 +219,7 @@ Template.viewer.onCreated(function() { syncMeasurementAndToolData(data); // Update each displayed viewport - var viewports = $('.imageViewerViewport').not('.empty'); - viewports.each(function(index, element) { - cornerstone.updateImage(element); - }); + updateAllViewports(); TrialResponseCriteria.validateAllDelayed(); }, @@ -235,10 +263,7 @@ Template.viewer.onCreated(function() { }); // Update each displayed viewport - var viewports = $('.imageViewerViewport').not('.empty'); - viewports.each(function(index, element) { - cornerstone.updateImage(element); - }); + updateAllViewports(); ValidationErrors.remove({ measurementId: data._id diff --git a/LesionTracker/client/components/viewerMain/viewerMain.js b/LesionTracker/client/components/viewerMain/viewerMain.js index 5f262583c..40bf15cfd 100644 --- a/LesionTracker/client/components/viewerMain/viewerMain.js +++ b/LesionTracker/client/components/viewerMain/viewerMain.js @@ -46,6 +46,13 @@ Template.viewerMain.helpers({ iconClasses: 'fa fa-arrows-v' }); + buttonData.push({ + id: 'ellipticalRoi', + title: 'Elliptical ROI Measurement', + classes: 'imageViewerTool', + iconClasses: 'fa fa-circle-o' + }); + buttonData.push({ id: 'lesion', title: 'Target Tool', diff --git a/Packages/cornerstone/client/cornerstoneTools.js b/Packages/cornerstone/client/cornerstoneTools.js index 3dafa86ff..369ef0549 100644 --- a/Packages/cornerstone/client/cornerstoneTools.js +++ b/Packages/cornerstone/client/cornerstoneTools.js @@ -3136,6 +3136,8 @@ if (typeof cornerstoneTools === 'undefined') { if (!isNaN(meanStdDev.mean) && !isNaN(meanStdDev.stdDev)) { data.meanStdDev = meanStdDev; + data.mean = meanStdDev.mean; + data.stdev = meanStdDev.stdev; } } @@ -3985,6 +3987,9 @@ if (typeof cornerstoneTools === 'undefined') { // Calculate the length, and create the text variable with the millimeters or pixels suffix var length = Math.sqrt(dx * dx + dy * dy); + // Store the length inside the tool for outside access + data.length = length; + // Set the length text suffix depending on whether or not pixelSpacing is available var suffix = ' mm'; if (!eventData.image.rowPixelSpacing || !eventData.image.columnPixelSpacing) { @@ -6646,7 +6651,7 @@ if (typeof cornerstoneTools === 'undefined') { // If any handle is outside the image, delete the tool data if (options.deleteIfHandleOutsideImage === true && cornerstoneTools.anyHandlesOutsideImage(eventData, data.handles)) { - cornerstoneTools.toolState.removeToolState(element, toolType, data); + cornerstoneTools.removeToolState(element, toolType, data); } cornerstone.updateImage(element); diff --git a/Packages/lesiontracker/both/collections.js b/Packages/lesiontracker/both/collections.js index 6dab1a18d..ee3101cd0 100644 --- a/Packages/lesiontracker/both/collections.js +++ b/Packages/lesiontracker/both/collections.js @@ -1,5 +1,6 @@ Timepoints = new Meteor.Collection('timepoints'); Studies = new Meteor.Collection('studies'); Measurements = new Meteor.Collection('measurements'); +ImageMeasurements = new Meteor.Collection('imageMeasurements'); WorklistSubscriptions = ['studies', 'timepoints']; \ No newline at end of file diff --git a/Packages/lesiontracker/client/compatibility/lesionTool.js b/Packages/lesiontracker/client/compatibility/lesionTool.js index ce0b1c471..f3e276c3f 100644 --- a/Packages/lesiontracker/client/compatibility/lesionTool.js +++ b/Packages/lesiontracker/client/compatibility/lesionTool.js @@ -99,7 +99,8 @@ longestDiameter: 0, shortestDiameter: 0, isDeleted: false, - isTarget: true + isTarget: true, + measurementType: 'bidirectional' }; return measurementData; } diff --git a/Packages/lesiontracker/client/compatibility/nonTargetTool.js b/Packages/lesiontracker/client/compatibility/nonTargetTool.js index 8f1146add..dcf8d02f8 100644 --- a/Packages/lesiontracker/client/compatibility/nonTargetTool.js +++ b/Packages/lesiontracker/client/compatibility/nonTargetTool.js @@ -153,7 +153,8 @@ studyInstanceUid: studyInstanceUid, patientId: patientId, response: '', - isTarget: false + isTarget: false, + measurementType: 'nonTarget' }; return measurementData; diff --git a/Packages/lesiontracker/client/components/lesionTable/lesionTable.js b/Packages/lesiontracker/client/components/lesionTable/lesionTable.js index 92c61327e..44273c230 100644 --- a/Packages/lesiontracker/client/components/lesionTable/lesionTable.js +++ b/Packages/lesiontracker/client/components/lesionTable/lesionTable.js @@ -1,7 +1,14 @@ Template.lesionTable.helpers({ measurement: function() { // All Targets shall be listed first followed by Non-Targets - return Measurements.find({}, { + return Measurements.find({ + measurementType: { + $in: [ + 'bidirectional', + 'nonTarget' + ] + } + }, { sort: { isTarget: -1, lesionNumberAbsolute: 1 diff --git a/Packages/lesiontracker/lib/LesionManager.js b/Packages/lesiontracker/lib/LesionManager.js index cba49ddb0..283bd8c4c 100644 --- a/Packages/lesiontracker/lib/LesionManager.js +++ b/Packages/lesiontracker/lib/LesionManager.js @@ -1,3 +1,17 @@ +measurementValuesByType = { + bidirectional: [ + 'shortestDiameter', + 'longestDiameter' + ], + nonTarget: ['response'], + length: ['length'], + ellipticalRoi: [ + 'area', + 'mean', + 'stdev' + ] +}; + /** * Update the Timepoint object for a specific Measurement. * If no measurement exists yet, one will be created. @@ -46,13 +60,18 @@ function updateLesionData(lesionData) { imageId: lesionData.imageId }; - if (lesionData.isTarget === true) { - timepointData.shortestDiameter = lesionData.shortestDiameter; - timepointData.longestDiameter = lesionData.longestDiameter; - } else { - timepointData.response = lesionData.response; + if (!lesionData.measurementType) { + // For debugging + log.warn('No MeasurementType available?'); } + // Populate this timepoint's data with whichever values + // are stored for this Measurement type + var values = measurementValuesByType[lesionData.measurementType]; + values.forEach(function(valueName) { + timepointData[valueName] = lesionData[valueName]; + }); + // If no such lesion exists, we need to add one if (!existingMeasurement) { // Create a data structure for the Measurement diff --git a/Packages/lesiontracker/lib/TrialResponseCriteria.js b/Packages/lesiontracker/lib/TrialResponseCriteria.js index f85746049..8461e4cd0 100644 --- a/Packages/lesiontracker/lib/TrialResponseCriteria.js +++ b/Packages/lesiontracker/lib/TrialResponseCriteria.js @@ -233,7 +233,6 @@ function validateSingleMeasurement(measurementData) { // If no such Measurement exists, stop here if (!measurement) { - log.warn('No Measurement found?'); return; } diff --git a/Packages/lesiontracker/lib/clearTools.js b/Packages/lesiontracker/lib/clearTools.js index ea599c115..2c7e6818e 100644 --- a/Packages/lesiontracker/lib/clearTools.js +++ b/Packages/lesiontracker/lib/clearTools.js @@ -1,6 +1,6 @@ clearTools = function() { var patientId = Session.get('patientId'); - var toolTypes = [ 'lesion', 'nonTarget', 'length' ]; + var toolTypes = [ 'lesion', 'nonTarget', 'length', 'ellipticalRoi']; var toolState = cornerstoneTools.globalImageIdSpecificToolStateManager.toolState; var toolStateKeys = Object.keys(toolState).slice(0); @@ -43,6 +43,7 @@ clearTools = function() { // Remove patient's measurements Meteor.call('removeMeasurementsByPatientId', patientId); + Meteor.call('removeImageMeasurementsByPatientId', patientId); // Clear all validation errors ValidationErrors.remove({}); diff --git a/Packages/lesiontracker/lib/handleMeasurementAdded.js b/Packages/lesiontracker/lib/handleMeasurementAdded.js index b5395ba6f..a94fdb019 100644 --- a/Packages/lesiontracker/lib/handleMeasurementAdded.js +++ b/Packages/lesiontracker/lib/handleMeasurementAdded.js @@ -7,6 +7,27 @@ handleMeasurementAdded = function(e, eventData) { log.info('CornerstoneToolsMeasurementAdded'); LesionManager.updateLesionData(measurementData); TrialResponseCriteria.validateDelayed(measurementData); + break; + case 'ellipticalRoi': + case 'length': + var enabledElement = cornerstone.getEnabledElement(eventData.element); + var imageId = enabledElement.image.imageId; + + // Get the studyInstanceUid and series metaData + var study = cornerstoneTools.metaData.get('study', imageId); + var series = cornerstoneTools.metaData.get('series', imageId); + + // Add the relevant metaData to this ImageMeasurement's toolData + measurementData.clientId = ClientId; + measurementData.imageId = imageId; + measurementData.measurementType = eventData.toolType; + measurementData.patientId = study.patientId; + measurementData.studyInstanceUid = study.studyInstanceUid; + measurementData.seriesInstanceUid = series.seriesInstanceUid; + + // Create the ImageMeasurement in the database + measurementData._id = ImageMeasurements.insert(measurementData); + break; } }; diff --git a/Packages/lesiontracker/lib/handleMeasurementModified.js b/Packages/lesiontracker/lib/handleMeasurementModified.js index 1384c2345..6d5844696 100644 --- a/Packages/lesiontracker/lib/handleMeasurementModified.js +++ b/Packages/lesiontracker/lib/handleMeasurementModified.js @@ -8,5 +8,31 @@ handleMeasurementModified = function(e, eventData) { LesionManager.updateLesionData(measurementData); TrialResponseCriteria.validateDelayed(measurementData); break; + case 'ellipticalRoi': + case 'length': + var enabledElement = cornerstone.getEnabledElement(eventData.element); + var imageId = enabledElement.image.imageId; + + // Get the studyInstanceUid and series metaData + var study = cornerstoneTools.metaData.get('study', imageId); + var series = cornerstoneTools.metaData.get('series', imageId); + + // Add the relevant metaData to this ImageMeasurement's toolData + measurementData.clientId = ClientId; + measurementData.imageId = imageId; + measurementData.measurementType = eventData.toolType; + measurementData.patientId = study.patientId; + measurementData.studyInstanceUid = study.studyInstanceUid; + measurementData.seriesInstanceUid = series.seriesInstanceUid; + var toUpdate = $.extend({}, measurementData); + + // Remove the Mongo _id otherwise it will complain when we try to 'mod' it. + delete toUpdate._id; + + // Update the ImageMeasurement in the database + ImageMeasurements.update(measurementData._id, { + $set: toUpdate + }); + break; } }; \ No newline at end of file diff --git a/Packages/lesiontracker/lib/removeToolDataWithMeasurementId.js b/Packages/lesiontracker/lib/removeToolDataWithMeasurementId.js index 864de1c0f..e8f5dbb90 100644 --- a/Packages/lesiontracker/lib/removeToolDataWithMeasurementId.js +++ b/Packages/lesiontracker/lib/removeToolDataWithMeasurementId.js @@ -15,13 +15,14 @@ removeToolDataWithMeasurementId = function(imageId, toolType, measurementId) { // Search toolData for entries linked to the specified Measurement var toRemove = []; toolData.forEach(function(measurement, index) { - if (measurement.id === measurementId) { + if (measurement.id === measurementId || + measurement._id === measurementId) { toRemove.push(index); return false; } }); - log.info("Removing Indices: "); + log.info('Removing Indices: '); log.info(toRemove); // If any toolData entries need to be removed, splice them from diff --git a/Packages/lesiontracker/lib/syncImageMeasurementAndToolData.js b/Packages/lesiontracker/lib/syncImageMeasurementAndToolData.js new file mode 100644 index 000000000..88a1be6d0 --- /dev/null +++ b/Packages/lesiontracker/lib/syncImageMeasurementAndToolData.js @@ -0,0 +1,59 @@ +syncImageMeasurementAndToolData = function(measurement) { + // TODO: Refactor this to merge it with syncMeasurementAndToolData somehow + log.info('syncImageMeasurementAndToolData'); + + var toolState = cornerstoneTools.globalImageIdSpecificToolStateManager.toolState; + + var imageId = measurement.imageId; + var toolType = measurement.measurementType; + + // If no tool state exists for this imageId, create an empty object to store it + if (!toolState[imageId]) { + toolState[imageId] = {}; + } + + // Check if we already have toolData for this imageId and toolType + if (toolState[imageId][toolType] && + toolState[imageId][toolType].data && + toolState[imageId][toolType].data.length) { + // If we have toolData, we should search it for any toolData + // related to the current ImageMeasurement + var toolData = toolState[imageId][toolType].data; + + // Create a flag so we know if we have successfully updated + // this ImageMeasurement's timepoint data in the toolData + var alreadyExists = false; + + // Loop through the toolData to search for this ImageMeasurement + toolData.forEach(function(tool) { + // Break the loop if this isn't the ImageMeasurement we are looking for + if (tool._id !== measurement._id) { + return; + } + + // If we find the ImageMeasurement, set the flag to True + alreadyExists = true; + + // Update the toolData from the ImageMeasurement data + $.extend(tool, measurement); + return false; + }); + + // If we found the ImageMeasurement we intended to update, we can stop + // this function here + if (alreadyExists === true) { + return; + } + } else { + // If no toolData exists for this toolType, create an empty array to hold some + toolState[imageId][toolType] = { + data: [] + }; + } + + // If we have reached this point, it means we haven't found the ImageMeasurement we are + // looking for in the current toolData. This means we need to add it. + + // Add the ImageMeasurementData into the toolData for this imageId + toolState[imageId][toolType].data.push(measurement); +}; \ No newline at end of file diff --git a/Packages/lesiontracker/package.js b/Packages/lesiontracker/package.js index c2ce367f1..c67a6aaf1 100644 --- a/Packages/lesiontracker/package.js +++ b/Packages/lesiontracker/package.js @@ -146,6 +146,7 @@ Package.onUse(function(api) { api.addFiles('lib/calculateTotalLesionBurden.js', 'client'); api.addFiles('lib/syncMeasurementAndToolData.js', 'client'); + api.addFiles('lib/syncImageMeasurementAndToolData.js', 'client'); api.addFiles('lib/updateRelatedElements.js', 'client'); api.addFiles('lib/handleMeasurementAdded.js', 'client'); @@ -159,6 +160,7 @@ Package.onUse(function(api) { api.export('handleMeasurementModified', 'client'); api.export('handleMeasurementRemoved', 'client'); api.export('syncMeasurementAndToolData', 'client'); + api.export('syncImageMeasurementAndToolData', 'client'); api.export('updateRelatedElements', 'client'); api.export('openNewTabWithTimepoint', 'client'); api.export('activateLesion', 'client'); @@ -184,6 +186,7 @@ Package.onUse(function(api) { api.export('LocationResponses', 'client'); // Export collections spanning both client and server + api.export('ImageMeasurements', [ 'client', 'server' ]); api.export('Measurements', [ 'client', 'server' ]); api.export('Studies', [ 'client', 'server' ]); api.export('Timepoints', [ 'client', 'server' ]); diff --git a/Packages/lesiontracker/server/collections.js b/Packages/lesiontracker/server/collections.js index 8024a80b0..b3f6ccf4d 100644 --- a/Packages/lesiontracker/server/collections.js +++ b/Packages/lesiontracker/server/collections.js @@ -24,6 +24,12 @@ Meteor.publish('singlePatientMeasurements', function(patientId) { }); }); +Meteor.publish('singlePatientImageMeasurements', function(patientId) { + return ImageMeasurements.find({ + patientId: patientId + }); +}); + // Temporary fix to drop all Collections on server restart // http://stackoverflow.com/questions/23891631/meteor-how-can-i-drop-all-mongo-collections-and-clear-all-data-on-startup Meteor.startup(function() { diff --git a/Packages/lesiontracker/server/removeCollections.js b/Packages/lesiontracker/server/removeCollections.js index 4e06565e6..fb80164d4 100644 --- a/Packages/lesiontracker/server/removeCollections.js +++ b/Packages/lesiontracker/server/removeCollections.js @@ -44,6 +44,11 @@ Meteor.methods({ patientId: patientId }); }, + removeImageMeasurementsByPatientId: function(patientId) { + ImageMeasurements.remove({ + patientId: patientId + }); + }, clearAllTimepoints: function() { Timepoints.remove({}); }, diff --git a/Packages/viewerbase/lib/updateAllViewports.js b/Packages/viewerbase/lib/updateAllViewports.js new file mode 100644 index 000000000..8137550a9 --- /dev/null +++ b/Packages/viewerbase/lib/updateAllViewports.js @@ -0,0 +1,6 @@ +updateAllViewports = function() { + var viewports = $('.imageViewerViewport').not('.empty'); + viewports.each(function(index, element) { + cornerstone.updateImage(element); + }); +}; \ No newline at end of file diff --git a/Packages/viewerbase/lib/viewportFunctions.js b/Packages/viewerbase/lib/viewportFunctions.js index 80106ed35..38b3c89db 100644 --- a/Packages/viewerbase/lib/viewportFunctions.js +++ b/Packages/viewerbase/lib/viewportFunctions.js @@ -1,5 +1,5 @@ getActiveViewportElement = function() { - var viewportIndex = Session.get("activeViewport"); + var viewportIndex = Session.get("activeViewport") || 0; return $('.imageViewerViewport').get(viewportIndex); }; diff --git a/Packages/viewerbase/package.js b/Packages/viewerbase/package.js index 8ece9f8b2..4f8540c1f 100644 --- a/Packages/viewerbase/package.js +++ b/Packages/viewerbase/package.js @@ -1,10 +1,10 @@ Package.describe({ - name: "viewerbase", - summary: "Shared components and functions for Meteor DICOM Viewers", - version: '0.0.1' + name: 'viewerbase', + summary: 'Shared components and functions for Meteor DICOM Viewers', + version: '0.0.1' }); -Package.onUse(function (api) { +Package.onUse(function(api) { api.versionsFrom('1.2.0.2'); api.use('standard-app-packages'); @@ -27,7 +27,6 @@ Package.onUse(function (api) { // TODO= Find a meteor package for this api.addFiles('client/compatibility/jquery.hotkeys.js', 'client'); - // ---------- Collections ---------- api.addFiles('client/collections.js', 'client'); @@ -128,12 +127,12 @@ Package.onUse(function (api) { api.addFiles('lib/WLPresets.js', 'client'); api.addFiles('lib/resizeViewportElements.js', 'client'); api.addFiles('lib/setFocusToActiveViewport.js', 'client'); + api.addFiles('lib/updateAllViewports.js', 'client'); api.addFiles('lib/encodeQueryData.js', 'server'); - //api.export('accountsConfig', 'client'); - api.export('resizeViewportElements','client'); - api.export('handleResize','client'); + api.export('resizeViewportElements', 'client'); + api.export('handleResize', 'client'); api.export('enableHotkeys', 'client'); api.export('enablePrefetchOnElement', 'client'); api.export('displayReferenceLines', 'client'); @@ -146,6 +145,7 @@ Package.onUse(function (api) { api.export('sortStudy', 'client'); api.export('updateOrientationMarkers', 'client'); api.export('setFocusToActiveViewport', 'client'); + api.export('updateAllViewports', 'client'); api.export('encodeQueryData', 'server'); // Viewer management objects