Added persistent single-image measurments (length, ellipse) (LT-37, LT-156)

This commit is contained in:
Erik Ziegler 2016-02-13 18:57:04 +01:00
parent b624d12762
commit 36ff46be41
20 changed files with 227 additions and 34 deletions

View File

@ -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

View File

@ -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',

View File

@ -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);

View File

@ -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'];

View File

@ -99,7 +99,8 @@
longestDiameter: 0,
shortestDiameter: 0,
isDeleted: false,
isTarget: true
isTarget: true,
measurementType: 'bidirectional'
};
return measurementData;
}

View File

@ -153,7 +153,8 @@
studyInstanceUid: studyInstanceUid,
patientId: patientId,
response: '',
isTarget: false
isTarget: false,
measurementType: 'nonTarget'
};
return measurementData;

View File

@ -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

View File

@ -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

View File

@ -233,7 +233,6 @@ function validateSingleMeasurement(measurementData) {
// If no such Measurement exists, stop here
if (!measurement) {
log.warn('No Measurement found?');
return;
}

View File

@ -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({});

View File

@ -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;
}
};

View File

@ -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;
}
};

View File

@ -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

View File

@ -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);
};

View File

@ -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' ]);

View File

@ -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() {

View File

@ -44,6 +44,11 @@ Meteor.methods({
patientId: patientId
});
},
removeImageMeasurementsByPatientId: function(patientId) {
ImageMeasurements.remove({
patientId: patientId
});
},
clearAllTimepoints: function() {
Timepoints.remove({});
},

View File

@ -0,0 +1,6 @@
updateAllViewports = function() {
var viewports = $('.imageViewerViewport').not('.empty');
viewports.each(function(index, element) {
cornerstone.updateImage(element);
});
};

View File

@ -1,5 +1,5 @@
getActiveViewportElement = function() {
var viewportIndex = Session.get("activeViewport");
var viewportIndex = Session.get("activeViewport") || 0;
return $('.imageViewerViewport').get(viewportIndex);
};

View File

@ -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