Remove some subscription / reactivity / caching bugs and the unnecessary PatientLocations collection

This commit is contained in:
Erik Ziegler 2016-02-09 23:46:04 +01:00
parent 7d42456f57
commit 9cde04bcfe
43 changed files with 321 additions and 234 deletions

View File

@ -1,16 +1,18 @@
<template name="viewer">
{{#if Template.subscriptionsReady}}
<div id="viewer">
{{>confirmDeleteDialog}}
{{>lesionLocationDialog}}
{{>nonTargetLesionDialog}}
{{>nonTargetResponseDialog}}
{{>timepointTextDialog}}
{{ >conformanceCheckFeedback }}
{{>conformanceCheckFeedback}}
{{>hidingPanel}}
<div id="viewportAndLesionTable">
{{>viewerMain }}
{{>lesionTable }}
</div>
</div>
{{else}}
{{>loadingText}}
{{/if}}
</template>

View File

@ -1,9 +1,14 @@
Session.setDefault('activeViewport', false);
ViewerStudies = new Meteor.Collection(null);
ViewerStudies._debugName = 'ViewerStudies';
Template.viewer.onCreated(function() {
// Attach the Window resize listener
$(window).on('resize', handleResize);
ValidationErrors.remove({});
var self = this;
var firstMeasurementsActivated = false;
var contentId = this.data.contentId;
@ -97,10 +102,11 @@ Template.viewer.onCreated(function() {
Session.set('activeViewport', ViewerData[contentId].activeViewport || false);
// Set lesion tool buttons as disabled if pixel spacing is not available for active element
this.autorun(pixelSpacingAutorunCheck);
self.autorun(pixelSpacingAutorunCheck);
// Update the ViewerStudies collection with the loaded studies
ViewerStudies = new Meteor.Collection(null);
ViewerStudies.remove({});
this.data.studies.forEach(function(study) {
study.selected = true;
ViewerStudies.insert(study);
@ -110,11 +116,17 @@ Template.viewer.onCreated(function() {
Session.set('patientId', patientId);
self.autorun(function() {
var patientId = Session.get('patientId');
self.subscribe('singlePatientTimepoints', patientId);
self.subscribe('singlePatientMeasurements', patientId);
var dataContext = Template.currentData();
self.subscribe('singlePatientAssociatedStudies', dataContext.studies[0].patientId);
self.subscribe('singlePatientTimepoints', dataContext.studies[0].patientId);
self.subscribe('singlePatientMeasurements', dataContext.studies[0].patientId);
var subscriptionsReady = self.subscriptionsReady();
console.log('autorun viewer.js. Ready: ' + subscriptionsReady);
if (subscriptionsReady) {
TrialResponseCriteria.validateAllDelayed();
if (self.subscriptionsReady()) {
ViewerStudies.find().observe({
added: function(study) {
// Find the relevant timepoint given the newly added study
@ -128,7 +140,7 @@ Template.viewer.onCreated(function() {
log.warn('Study added to Viewer has not been associated!');
return;
}
// Update the added document with its related timepointId
ViewerStudies.update(study._id, {
$set: {

View File

@ -0,0 +1,68 @@
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();
}
});

View File

@ -1,4 +1,5 @@
LesionLocations = new Meteor.Collection(null);
LesionLocations._debugName = 'LesionLocations';
var organGroups = [
'Abdominal/Chest Wall',

View File

@ -1,4 +1,5 @@
LocationResponses = new Meteor.Collection(null);
LocationResponses._debugName = 'LocationResponses';
LocationResponses.insert({
text: 'Complete response',

View File

@ -1 +0,0 @@
PatientLocations = new Meteor.Collection(null);

View File

@ -194,11 +194,10 @@
y: mouseEventData.currentPoints.image.y - 70,
pointNearHandle: pointNearTextBox,
active: false,
movesIndependently: true,
movesIndependently: false,
drawnIndependently: true,
allowedOutsideImage: true
},
perpendicularStart: {
x: mouseEventData.currentPoints.image.x,
y: mouseEventData.currentPoints.image.y,
@ -208,7 +207,6 @@
drawnIndependently: true,
index: 2
},
perpendicularEnd: {
x: mouseEventData.currentPoints.image.x,
y: mouseEventData.currentPoints.image.y,
@ -217,7 +215,6 @@
drawnIndependently: true,
index: 3
}
},
imageId: imageId,
seriesInstanceUid: seriesInstanceUid,

View File

@ -141,7 +141,7 @@
y: mouseEventData.currentPoints.image.y - 50,
pointNearHandle: pointNearTextBox,
active: false,
movesIndependently: true,
movesIndependently: false,
drawnIndependently: true,
allowedOutsideImage: true
}

View File

@ -59,18 +59,53 @@ Template.associationModal.events({
// Sort the study dates, so we can get a range for these values
studyDates = studyDates.sort();
// Create a new timepoint to represent the (baseline or follow-up) studies
var timepoint = {
timepointType: timepointType,
timepointId: uuid.new(),
studyInstanceUids: studyInstanceUids,
patientId: relatedStudies[0].patientId, // TODO: Revisit this (Should timepoints be related to patientId?)
earliestDate: studyDates[0].format('YYYYMMDD'),
latestDate: studyDates[studyDates.length - 1].format('YYYYMMDD')
};
// Check if these studies are already associated with an existing Timepoint
var existingTimepoint;
if (timepointType === 'baseline') {
// If we're trying to associate them to the Baseline, we don't need to
// check if the studyInstanceUids are already associated with anything else
existingTimepoint = Timepoints.findOne({
patientId: relatedStudies[0].patientId,
timepointType: 'baseline'
});
} else {
// If we're trying to associate them to a Follow-up, we should check if any
// of them are already part of a Follow-up (e.g. Follow-up 1), so that
// the rest will also be associated with Follow-up 1.
existingTimepoint = Timepoints.findOne({
patientId: relatedStudies[0].patientId,
studyInstanceUids: {
$in: studyInstanceUids
}
});
}
// Insert this timepoint into the Timepoints Collection
Timepoints.insert(timepoint);
var timepointId;
if (existingTimepoint) {
// If these studies are already associated with an existing Timepoint,
// and the desired timepoint type is the same (e.g. Follow-up), update
// this Timepoint instead of creating a new one
Timepoints.update(existingTimepoint._id, {
$set: {
studyInstanceUids: studyInstanceUids
}
});
timepointId = existingTimepoint.timepointId;
} else {
// Create a new timepoint to represent the (baseline or follow-up) studies
var timepoint = {
timepointType: timepointType,
timepointId: uuid.new(),
studyInstanceUids: studyInstanceUids,
patientId: relatedStudies[0].patientId,
earliestDate: studyDates[0].format('YYYYMMDD'),
latestDate: studyDates[studyDates.length - 1].format('YYYYMMDD')
};
// Insert this timepoint into the Timepoints Collection
Timepoints.insert(timepoint);
timepointId = timepoint.timepointId;
}
// Loop through these studies to associate them with the newly created timepoint
relatedStudies.forEach(function(study) {
@ -83,7 +118,7 @@ Template.associationModal.events({
// If a study already exists, update the entry with the new timepointId
Studies.update(existingStudy._id, {
$set: {
timepointId: timepoint.timepointId
timepointId: timepointId
}
});
} else {
@ -93,7 +128,7 @@ Template.associationModal.events({
delete study._id;
// Attach the timepointId and insert it into the Studies Collection
study.timepointId = timepoint.timepointId;
study.timepointId = timepointId;
Studies.insert(study);
}
});

View File

@ -205,20 +205,6 @@ Template.lesionLocationDialog.events({
_id: selectedOptionId
});
var id;
var existingLocation = PatientLocations.findOne({
location: locationObj.location
});
if (existingLocation) {
id = existingLocation._id;
} else {
// Adds location data to PatientLocation and retrieve the location ID
id = PatientLocations.insert({
location: locationObj.location,
locationId: locationObj._id
});
}
// Add an ID value to the tool data to link it to the Measurements collection
if (!measurementData.id) {
measurementData.id = 'notready';
@ -226,7 +212,6 @@ Template.lesionLocationDialog.events({
// Link locationUID with active lesion measurementData
measurementData.location = locationObj.location;
measurementData.locationId = locationObj.id;
measurementData.locationUID = id;
/// Set the isTarget value to true, since this is the target-lesion dialog callback
measurementData.isTarget = true;
@ -241,8 +226,7 @@ Template.lesionLocationDialog.events({
$set: {
location: locationObj.location,
locationId: locationObj.id,
isNodal: locationObj.isNodal,
locationUID: id
isNodal: locationObj.isNodal
}
});
}

View File

@ -31,10 +31,10 @@ function removeTimepointAssociations() {
return;
}
// Update the Studies Collection to remove the link to this Timepoint
Studies.update(study._id, {
unset: {
timepointId: ''
// Remove this entry from the Studies Collection
Meteor.call('removeAssociatedStudy', study._id, function(error) {
if (error) {
log.warn(error);
}
});

View File

@ -70,27 +70,28 @@ function getLesionLocationCallback(measurementData, eventData) {
// Find out if this lesion number is already added in the lesion manager for another timepoint
// If it is, disable selector location
var locationUID = LesionManager.lesionNumberExists(measurementData);
if (locationUID) {
var locationId = LesionManager.lesionNumberExists(measurementData);
if (locationId) {
// Add an ID value to the tool data to link it to the Measurements collection
measurementData.id = 'notready';
measurementData.locationUID = locationUID;
measurementData.locationId = locationId;
// Disable the selection of a new location
disableLocationSelection(measurementData.locationUID);
disableLocationSelection(measurementData.locationId);
}
// Disable selector location to prevent selecting a new location
function disableLocationSelection(locationUID) {
var locationName = LesionManager.getLocationName(locationUID);
selectorLocation.find('option').each(function() {
if ($(this).text() === locationName) {
// Select location in locations dropdown list
selectorLocation.find('option').eq($(this).index()).prop('selected', true);
}
});
function disableLocationSelection(locationId) {
var locationObject = LesionLocations.findOne({
id: locationId
});
if (!locationObject) {
return;
}
selectorLocation.find('option[value="' + locationObject._id + '"]').prop('selected', true);
selectorLocation.prop('disabled', true);
}
@ -273,26 +274,12 @@ Template.nonTargetLesionDialog.events({
_id: selectedOptionId
});
var id;
var existingLocation = PatientLocations.findOne({
location: locationObj.location
});
if (existingLocation) {
id = existingLocation._id;
} else {
// Adds location data to PatientLocation and retrieve the location ID
id = PatientLocations.insert({
location: locationObj.location
});
}
if (measurementData.id) {
// Update the location data
Measurements.update(measurementData.id, {
$set: {
location: locationObj.location,
locationId: locationObj.id,
locationUID: id
}
});
} else {
@ -300,9 +287,6 @@ Template.nonTargetLesionDialog.events({
measurementData.id = 'notready';
}
// Link locationUID with active lesion measurementData
measurementData.locationUID = id;
/// Set the isTarget value to true, since this is the target-lesion dialog callback
measurementData.isTarget = false;

View File

@ -76,12 +76,7 @@ Template.studyDateList.events({
var loadingIndicator = selectBox.siblings('.loading');
loadingIndicator.css('display', 'block');
Meteor.call('GetStudyMetadata', studyInstanceUid, function(error, study) {
if (error) {
log.warn(error);
return;
}
getStudyMetadata(studyInstanceUid, function(study) {
sortStudy(study);
// Hide the loading indicator

View File

@ -1,12 +0,0 @@
<template name="timepointTextDialog">
<div id="timepointTextDialog">
<div class="timepointDialogContentWrapper">
<div class="dialogContent">
<div class="timepointContent">
<input type="checkbox" id="checkBoxBaseline" value="ok">
<label for="checkBoxBaseline">Baseline</label>
</div>
</div>
</div>
</div>
</template>

View File

@ -1,25 +0,0 @@
#timepointTextDialog
display: none
position: absolute
z-index: 99
.timepointDialogContentWrapper
position: absolute
width: 100px
height: 30px
padding: 5px
background-color: rgba(255,255,255,1)
border-top-left-radius: 5px
border-top-right-radius: 5px
.dialogContent
margin: 0 auto
float: none
padding: 0
.timepointContent
font-size: 16px
#checkBoxBaseline
height: 16px
width: 16px

View File

@ -1,20 +1,3 @@
/**
* Retrieve a location name (e.g. Liver Right) from the
* PatientLocations Collection by id, if it exists. Otherwise,
* return an empty string.
*
* @param id
* @returns {*|string}
*/
function getLocationName(id) {
var locationObject = PatientLocations.findOne(id);
if (!locationObject || !locationObject.location) {
return '';
}
return locationObject.location;
}
/**
* Update the Timepoint object for a specific Measurement.
* If no measurement exists yet, one will be created.
@ -33,7 +16,6 @@ function updateLesionData(lesionData) {
return;
}
var timepoint = Timepoints.findOne({
timepointId: study.timepointId
});
@ -84,7 +66,7 @@ function updateLesionData(lesionData) {
// Retrieve the location name given the locationUID
if (lesionData.locationUID !== undefined) {
var locationObj = PatientLocations.findOne({
var locationObj = LesionLocations.findOne({
locationUID: lesionData.locationUID
});
@ -191,12 +173,11 @@ function lesionNumberExists(lesionData) {
return;
}
return measurement.locationUID;
return measurement.locationId;
}
LesionManager = {
updateLesionData: updateLesionData,
getNewLesionNumber: getNewLesionNumber,
lesionNumberExists: lesionNumberExists,
getLocationName: getLocationName
lesionNumberExists: lesionNumberExists
};

View File

@ -50,7 +50,7 @@ function RECIST(image) {
shortestDiameter: {
numericality: {
greaterThanOrEqualTo: 15,
message: '^Nodal target lesions must be >= %{count} mm short axis'
//message: '^Nodal target lesions must be >= %{count} mm short axis'
}
}
};
@ -72,7 +72,7 @@ function RECIST(image) {
totalLesionBurden: {
numericality: {
greaterThanOrEqualTo: 2, // TODO: Check this, the value wasn't specified!
message: '^Total lesion burden (SPD target lesions + SPD new lesions) should be greater than %{count}.'
//message: '^Total lesion burden (SPD target lesions + SPD new lesions) should be greater than %{count}.'
}
}
};
@ -126,7 +126,7 @@ function RECIST(image) {
longestDiameter: {
numericality: {
greaterThanOrEqualTo: 20,
message: '^Extranodal lesions must be >= %{count} mm on chest X-ray'
//message: '^Extranodal lesions must be >= %{count} mm on chest X-ray'
}
}
};
@ -135,7 +135,7 @@ function RECIST(image) {
longestDiameter: {
numericality: {
greaterThanOrEqualTo: 10,
message: '^Extranodal target lesions must be >= %{count} mm long axis'
//message: '^Extranodal target lesions must be >= %{count} mm long axis'
}
}
};
@ -340,6 +340,9 @@ getTrialCriteriaConstraints = function(criteriaType, imageId) {
// Otherwise, retrieve the series metaData to identify the modality of the image
var seriesMetaData = cornerstoneTools.metaData.get('series', imageId);
if (!seriesMetaData) {
return;
}
// TODO: Get the rest of the metaData that has already been loaded by Cornerstone
var image = {};
@ -363,7 +366,6 @@ getTrialCriteriaConstraints = function(criteriaType, imageId) {
});
if (!study) {
log.warn('No study/timepoint association.');
return;
}

View File

@ -1,5 +1,6 @@
// Create a client-only Collection to store our Validation Errors
ValidationErrors = new Meteor.Collection(null);
ValidationErrors._debugName = 'ValidationErrors';
// Set Validate.js Library's default options
validate.options = {
@ -216,8 +217,8 @@ function validateSingleMeasurement(measurementData) {
var criteriaType = Session.get('TrialResponseAssessmentCriteria');
var currentConstraints = getTrialCriteriaConstraints(criteriaType, measurementData.imageId);
// If we have no relevant constraints, stop here
if (!currentConstraints) {
log.warn('No relevant contraints could be applied');
return;
}
@ -236,6 +237,11 @@ function validateSingleMeasurement(measurementData) {
// Find the specific measurement data for this Measurement at this Timepoint
var currentMeasurement = measurement.timepoints[timepointId];
// Return here if the measurement was removed during validation
if (!currentMeasurement) {
return;
}
// Include target and nodal flags on the timepoint-specific data so it is easier to validate
// TODO: Rethink what to pass to assessSingleMeasurement?
currentMeasurement.isTarget = measurement.isTarget;
@ -256,10 +262,11 @@ function validateGroups() {
var criteriaType = Session.get('TrialResponseAssessmentCriteria');
Timepoints.find().forEach(function(timepoint) {
// TODO: Criteria for the specific image are retrieved from the general set of criteria.
// - The acquisitionSliceThickness, for example, may be pulled from the image metadata
// - The organ in question, e.g. Chest X-ray, may determine the exact specifications for the current trial criteria
// Criteria for the specific image are retrieved from the general set of criteria.
var currentConstraints = getTrialCriteriaConstraints(criteriaType);
if (!currentConstraints) {
return;
}
// Retrieve the current constraints which apply to the specific Timepoint type
// (e.g. baseline, followup) that this Measurement is being edited on.
@ -289,12 +296,9 @@ function validateAll() {
currentMeasurement.lesionNumber = measurement.lesionNumber;
currentMeasurement._id = measurement._id;
// TODO: Criteria for the specific image are retrieved from the general set of criteria.
// - The acquisitionSliceThickness, for example, may be pulled from the image metadata
// - The organ in question, e.g. Chest X-ray, may determine the exact specifications for the current trial criteria
// Criteria for the specific image are retrieved from the general set of criteria.
var currentConstraints = getTrialCriteriaConstraints(criteriaType, currentMeasurement.imageId);
if (!currentConstraints) {
log.warn('No relevant contraints could be applied');
return;
}

View File

@ -12,6 +12,10 @@ activateMeasurements = function(element, measurementId, templateData, viewportIn
var timepointData = getTimepointObject(imageId);
var measurementData = Measurements.findOne(measurementId);
if (!timepointData) {
return;
}
var measurementAtTimepoint = measurementData.timepoints[timepointData.timepointId];
if (!measurementAtTimepoint) {
return;

View File

@ -5,11 +5,15 @@ clearTools = function() {
var toolStateKeys = Object.keys(toolState).slice(0);
var viewportElements = $('.imageViewerViewport').not('.empty');
var seriesInstanceUIds = []; // Holds seriesInstanceUId of imageViewerViewport elements
var seriesInstanceUids = []; // Holds seriesInstanceUid of imageViewerViewport elements
viewportElements.each(function(index, element) {
var enabledElement = cornerstone.getEnabledElement(element);
var series = cornerstoneTools.metaData.get('series', enabledElement.image.imageId);
seriesInstanceUIds.push(series.seriesInstanceUid);
if (!series) {
return;
}
seriesInstanceUids.push(series.seriesInstanceUid);
});
// Set null array for toolState data found by imageId and toolType
@ -18,8 +22,12 @@ clearTools = function() {
var toolTypeData = toolState[imageId][toolType];
if (toolTypeData && toolTypeData.data.length > 0) {
var series = cornerstoneTools.metaData.get('series', imageId);
// If seriesInstanceUid is found in seriesInstanceUIds, set toolState data as null
if (seriesInstanceUIds.indexOf(series.seriesInstanceUid) > -1) {
if (!series) {
return;
}
// If seriesInstanceUid is found in seriesInstanceUids, set toolState data as null
if (seriesInstanceUids.indexOf(series.seriesInstanceUid) > -1) {
toolState[imageId][toolType] = {
data: []
};

View File

@ -1,10 +1,10 @@
handleMeasurementAdded = function(e, eventData) {
log.info('CornerstoneToolsMeasurementAdded');
var measurementData = eventData.measurementData;
switch (eventData.toolType) {
case 'nonTarget':
case 'lesion':
log.info('CornerstoneToolsMeasurementAdded');
LesionManager.updateLesionData(measurementData);
TrialResponseCriteria.validateDelayed(measurementData);
break;

View File

@ -1,10 +1,10 @@
handleMeasurementModified = function(e, eventData) {
log.info('CornerstoneToolsMeasurementModified');
var measurementData = eventData.measurementData;
switch (eventData.toolType) {
case 'nonTarget':
case 'lesion':
log.info('CornerstoneToolsMeasurementModified');
LesionManager.updateLesionData(measurementData);
TrialResponseCriteria.validateDelayed(measurementData);
break;

View File

@ -62,7 +62,7 @@ syncMeasurementAndToolData = function(data) {
measurementData.isDeleted = data.isDeleted;
measurementData.location = data.location;
measurementData.locationUID = data.locationUID;
measurementData.patientId = patientId;
measurementData.patientId = data.patientId;
measurementData.visible = data.visible;
measurementData.active = data.active;
measurementData.uid = data.uid;

View File

@ -33,5 +33,8 @@ function dblClickOnStudy(data) {
return;
}
// Add the Timepoint name to the Patient name to create the tab title
title += ' ' + getTimepointName(timepoint);
openNewTabWithTimepoint(timepoint.timepointId, title);
}

View File

@ -29,7 +29,6 @@ Package.onUse(function(api) {
// Client-side collections
api.addFiles('client/collections/LesionLocations.js', 'client');
api.addFiles('client/collections/LocationResponses.js', 'client');
api.addFiles('client/collections/PatientLocations.js', 'client');
// Additional Custom Cornerstone Tools for Lesion Tracker
api.addFiles('client/compatibility/lesionTool.js', 'client', {
@ -108,9 +107,6 @@ Package.onUse(function(api) {
api.addFiles('client/components/nonTargetResponseDialog/nonTargetResponseDialog.styl', 'client');
api.addFiles('client/components/nonTargetResponseDialog/nonTargetResponseDialog.js', 'client');
api.addFiles('client/components/timepointTextDialog/timepointTextDialog.html', 'client');
api.addFiles('client/components/timepointTextDialog/timepointTextDialog.styl', 'client');
api.addFiles('client/components/lesionTrackerWorklistStudy/lesionTrackerWorklistStudy.html', 'client');
api.addFiles('client/components/lesionTrackerWorklistStudy/lesionTrackerWorklistStudy.styl', 'client');
api.addFiles('client/components/lesionTrackerWorklistStudy/lesionTrackerWorklistStudy.js', 'client');
@ -186,7 +182,6 @@ Package.onUse(function(api) {
api.export('ValidationErrors', 'client');
api.export('LesionLocations', 'client');
api.export('LocationResponses', 'client');
api.export('PatientLocations', 'client');
// Export collections spanning both client and server
api.export('Measurements', [ 'client', 'server' ]);

View File

@ -12,6 +12,12 @@ Meteor.publish('studies', function() {
return Studies.find();
});
Meteor.publish('singlePatientAssociatedStudies', function(patientId) {
return Studies.find({
patientId: patientId
});
});
Meteor.publish('singlePatientMeasurements', function(patientId) {
return Measurements.find({
patientId: patientId

View File

@ -49,5 +49,8 @@ Meteor.methods({
},
removeTimepoint: function(id) {
Timepoints.remove(id);
},
removeAssociatedStudy: function(id) {
Studies.remove(id);
}
});

View File

@ -28,10 +28,10 @@ Template.imageThumbnail.onRendered(function() {
});
Template.imageThumbnail.helpers({
'percentComplete': function() {
percentComplete: function() {
var percentComplete = Session.get('CornerstoneThumbnailLoadProgress' + this.thumbnailIndex);
if (percentComplete && percentComplete !== 100) {
return percentComplete + '%';
}
}
});
});

View File

@ -1,11 +1,11 @@
Template.hangingProtocolButtons.helpers({
'isNextAvailable': function() {
isNextAvailable: function() {
var presentationGroup = Session.get('WindowManagerPresentationGroup');
var numPresentationGroups = WindowManager.getNumPresentationGroups();
return presentationGroup < numPresentationGroups;
},
'isPreviousAvailable': function() {
isPreviousAvailable: function() {
var presentationGroup = Session.get('WindowManagerPresentationGroup');
return presentationGroup > 1;
}
});
});

View File

@ -411,6 +411,10 @@ Meteor.startup(function() {
cornerstoneTools.magnify.setConfiguration(config);
});
Template.imageViewerViewport.onCreated(function() {
console.log('imageViewerViewport onCreated');
});
Template.imageViewerViewport.onRendered(function() {
var templateData = Template.currentData();
log.info('imageViewerViewport onRendered');
@ -455,7 +459,7 @@ Template.imageViewerViewport.onRendered(function() {
// TODO: This code block might be refactored
// Load previous measurement study when reloading a patient
if (!study) {
Meteor.call('GetStudyMetadata', this.data.studyInstanceUid, function(error, study) {
getStudyMetadata(this.data.studyInstanceUid, function(study) {
// Once we have retrieved the data, we sort the series' by series
// and instance number in ascending order
if (!study) {
@ -466,7 +470,7 @@ Template.imageViewerViewport.onRendered(function() {
data.study = study;
setSeries(data, seriesInstanceUid, templateData);
});
})
}
data.study = study;
@ -482,7 +486,10 @@ Template.imageViewerViewport.onDestroyed(function() {
// Try to stop any currently playing clips
// Otherwise the interval will continuously throw errors
try {
cornerstoneTools.stopClip(element);
var enabledElement = cornerstone.getEnabledElement(element);
if (enabledElement) {
cornerstoneTools.stopClip(element);
}
} catch (error) {
log.warn(error);
}

View File

@ -1,4 +1,5 @@
ViewerWindows = new Meteor.Collection(null);
ViewerWindows._debugName = 'ViewerWindows';
Template.imageViewerViewports.helpers({
height: function() {
@ -10,14 +11,16 @@ Template.imageViewerViewports.helpers({
return 100 / viewportColumns;
},
viewerWindow: function() {
ViewerWindows = new Meteor.Collection(null);
log.info('ViewerWindows');
//log.info(ViewerWindows.find().fetch());
ViewerWindows.remove({});
log.info("imageViewerViewports viewportArray");
log.info('imageViewerViewports viewportArray');
var viewportRows = this.viewportRows || 1;
var viewportColumns = this.viewportColumns || 1;
var contentId = this.contentId || $("#viewer").parents(".tab-pane.active").attr('id');
var contentId = this.contentId || $('#viewer').parents('.tab-pane.active').attr('id');
if (this.viewportRows && this.viewportColumns) {
viewportRows = this.viewportRows || 1;
viewportColumns = this.viewportColumns || 1;
@ -47,13 +50,13 @@ Template.imageViewerViewports.helpers({
// Update viewerData
ViewerData[contentId].viewportRows = viewportRows;
ViewerData[contentId].viewportColumns = viewportColumns;
Session.set("ViewerData", ViewerData);
Session.set('ViewerData', ViewerData);
this.viewportRows = viewportRows;
this.viewportColumns = viewportColumns;
var numViewports = viewportRows * viewportColumns;
for (var i=0; i < numViewports; ++i) {
for (var i = 0; i < numViewports; ++i) {
var data = {
viewportIndex: i,
// These two are necessary because otherwise the width and height helpers
@ -77,14 +80,17 @@ Template.imageViewerViewports.helpers({
ViewerWindows.insert(data);
}
// Here we will find out if we need to load any other studies into the viewer
// We will make a list of unique studyInstanceUids
var uniqueStudyInstanceUids = [];
// Meteor doesn't support Mongo's 'distinct' function, so we have to do this in a loop
ViewerWindows.find().forEach(function(window) {
var windows = ViewerWindows.find({}, {
reactive: false
}).fetch();
windows.forEach(function(window) {
var studyInstanceUid = window.studyInstanceUid;
if (!studyInstanceUid) {
return;
@ -99,10 +105,17 @@ Template.imageViewerViewports.helpers({
uniqueStudyInstanceUids.push(studyInstanceUid);
// If any of the associated studies is not already loaded, load it now
var loadedStudy = ViewerStudies.findOne({studyInstanceUid: studyInstanceUid});
var loadedStudy = ViewerStudies.findOne({
studyInstanceUid: studyInstanceUid
}, {
reactive: false
});
if (!loadedStudy) {
// Load the study
Meteor.call('GetStudyMetadata', studyInstanceUid, function(error, study) {
getStudyMetadata(studyInstanceUid, function(study) {
log.info("imageViewerViewports GetStudyMetadata: " + studyInstanceUid);
// Sort the study's series and instances by series and instance number
sortStudy(study);
@ -112,7 +125,9 @@ Template.imageViewerViewports.helpers({
}
});
return ViewerWindows.find();
return ViewerWindows.find({}, {
reactive: false
}).fetch();
}
});
@ -122,9 +137,9 @@ var savedSeriesData,
Template.imageViewerViewports.events({
'CornerstoneMouseDoubleClick .imageViewerViewport': function(e) {
var container = $(".viewerMain").get(0);
var container = $('.viewerMain').get(0);
var data;
var contentId = this.contentId || $("#viewer").parents(".tab-pane.active").attr('id');
var contentId = this.contentId || $('#viewer').parents('.tab-pane.active').attr('id');
// If there is more than one viewport on screen
// And one of them is double-clicked, it should be rendered alone
@ -183,4 +198,4 @@ Template.imageViewerViewports.events({
$('.imageViewerViewport').eq(0).addClass('zoomed');
}
}
});
});

View File

@ -59,7 +59,7 @@ function resultDataToStudyMetadata(resultData) {
*/
Services.DIMSE.Instances = function(studyInstanceUid) {
//var url = buildUrl(server, studyInstanceUid);
var result = DIMSE.retrieveInstances(studyInstanceUid);
var result = DIMSE.retrieveInstances(studyInstanceUid, null, {0x00080018 : ""});
console.log("DIMSE Instance retrieval");
console.log(result);

View File

@ -1,5 +1,10 @@
<template name="tabContent">
<div role="tabpanel" class="tab-pane {{active}}" id="{{contentid}}">
{{ >loadingText }}
{{>loadingText}}
<div class="viewerContainer">
<!-- This extra viewerContainer div only exists to be destroyed when switching tabs.
It is a workaround because Meteor's onDestroyed and destruction handlers don't fire
when removing the actual template from the DOM.-->
</div>
</div>
</template>

View File

@ -1,4 +1,8 @@
.tab-pane
.loadingTextDiv
h5
color: #777
color: #777
.viewerContainer
height: 100%
width: 100%

View File

@ -7,9 +7,9 @@
{{ >tabTitle }}
{{ /each }}
</ul>
<div class="tab-content">
<div id="worklistTabs" class="tab-content">
<div role="tabpanel" class="tab-pane active" id="worklistTab">
<div id="worklistContainer" class="container">
<div class="worklistContainer">
{{> worklistResult }}
</div>
</div>

View File

@ -11,20 +11,13 @@ ViewerData = Session.get('ViewerData') || {};
// Create the WorklistTabs collection
WorklistTabs = new Meteor.Collection(null);
WorklistTabs._debugName = 'WorklistTabs';
// Define the WorklistStudies Collection
// This is a client-side only Collection which
// Stores the list of studies in the Worklist
WorklistStudies = new Meteor.Collection(null);
Template.worklist.onCreated(function() {
var self = this;
if (Worklist.subscriptions) {
Worklist.subscriptions.forEach(function(collectionName) {
self.subscribe(collectionName);
});
}
});
WorklistStudies._debugName = 'WorklistStudies';
Template.worklist.onRendered(function() {
// If there is a tab set as active in the Session,

View File

@ -9,37 +9,18 @@ body
-ms-user-select: none
user-select: none
input.worklist-search
height: 25px
background-color: #888888
width: 100%
#tblStudyList
tr
height: 20px
.patient-name-input
width: 100%
.worklist-input
width: 100%
.modality-input
width: 100%
.patientid-input
width: 100%
.study-description-input
width: 100%
#worklistTab
background-color: #202020
#worklistContainer
.worklistContainer
background-color: #202020
margin: 0 auto
color: white
width:90%
width: 90%
padding-top: 10px
#tabs
@ -99,7 +80,7 @@ input.worklist-search
.tab-content
width: 100%
height: calc(100% - 91px)
.tab-pane
width: 100%
height: 100%

View File

@ -1,5 +1,5 @@
<template name="worklistResult">
<table id="tblStudyList" class="table table-striped noselect">
<table id="tblStudyList" class="worklistResult table table-striped noselect">
<thead>
<tr>
<th>

View File

@ -115,6 +115,20 @@ function search() {
});
}
Template.worklistResult.onCreated(function() {
console.log('WorklistResult onCreated!');
var self = this;
if (Worklist.subscriptions) {
Worklist.subscriptions.forEach(function(collectionName) {
self.subscribe(collectionName);
});
}
});
Template.worklistResult.onDestroyed(function() {
console.log('WorklistResult onDestroyed!');
});
Template.worklistResult.events({
'keydown input': function(e) {
if (e.which === 13) { // Enter

View File

@ -15,8 +15,14 @@ table#tblStudyList
#numImages.worklist-search, #modality.worklist-search
visibility: hidden
input.worklist-search
height: 25px
background-color: #888888
width: 100%
tbody
tr
height: 20px
padding: 4px
border: 1px solid #282828
background-color: black

View File

@ -1,5 +1,6 @@
// Maybe we should use regular Worklist collection?
WorklistSelectedStudies = new Meteor.Collection(null);
WorklistSelectedStudies._debugName = 'WorklistSelectedStudies';
function handleShiftClick(studyRow, data) {
log.info('shiftKey');

View File

@ -12,8 +12,6 @@ var StudyMetaData = {};
* @param failCallback The callback function to be executed when the study retrieval has failed
*/
getStudyMetadata = function(studyInstanceUid, doneCallback, failCallback) {
log.info('worklistStudy getStudyMetadata');
// If the StudyMetaData cache already has data related to this
// studyInstanceUid, then we should fire the doneCallback with this data
// and stop here.
@ -26,6 +24,8 @@ getStudyMetadata = function(studyInstanceUid, doneCallback, failCallback) {
// If no study metadata is in the cache variable, we need to retrieve it from
// the server with a call.
Meteor.call('GetStudyMetadata', studyInstanceUid, function(error, study) {
log.info('worklistStudy getStudyMetadata: ' + studyInstanceUid);
if (error) {
log.warn(error);
failCallback(error);

View File

@ -8,6 +8,7 @@ switchToTab = function(contentId) {
if (!contentId) {
return;
}
log.info('Switching to tab: ' + contentId);
// Use Bootstrap's Tab JavaScript to show the contents of the current tab
@ -15,7 +16,8 @@ switchToTab = function(contentId) {
$('.tabTitle a[data-target="#' + contentId + '"]').tab('show');
// Remove any previous Viewers from the DOM
$('#viewer').remove();
$('.viewerContainer').remove();
$('.worklistContainer').remove();
// Update the 'activeContentId' variable in Session
Session.set('activeContentId', contentId);
@ -23,7 +25,16 @@ switchToTab = function(contentId) {
// If we are switching to the Worklist tab, reset any CSS styles
// that have been applied to prevent scrolling in the Viewer.
// Then stop here, since nothing needs to be re-rendered.
var container;
if (contentId === 'worklistTab') {
container = $('.tab-content').find('#worklistTab').get(0);
var worklistContainer = document.createElement('div');
worklistContainer.classList.add('worklistContainer');
container.appendChild(worklistContainer);
// Use Blaze to render the WorklistResult Template into the container
Blaze.render(Template.worklistResult, worklistContainer);
document.body.style.overflow = null;
document.body.style.height = null;
document.body.style.minWidth = null;
@ -67,10 +78,13 @@ switchToTab = function(contentId) {
}
// Remove the loading text template that is inside the tab container by default
var viewerContainer = document.createElement('div');
viewerContainer.classList.add('viewerContainer');
container.innerHTML = '';
container.appendChild(viewerContainer);
// Use Blaze to render the Viewer Template into the container
UI.renderWithData(Template.viewer, data, container);
Blaze.renderWithData(Template.viewer, data, viewerContainer);
// Retrieve the DOM element of the viewer
var imageViewer = $('#viewer');
@ -87,4 +101,4 @@ switchToTab = function(contentId) {
document.body.style.position = 'fixed';
}
});
};
};