418 lines
15 KiB
JavaScript
418 lines
15 KiB
JavaScript
import { Mongo } from 'meteor/mongo';
|
|
import { Tracker } from 'meteor/tracker';
|
|
import { _ } from 'meteor/underscore';
|
|
import { OHIF } from 'meteor/ohif:core';
|
|
|
|
let configuration = {};
|
|
|
|
class MeasurementApi {
|
|
static setConfiguration(config) {
|
|
_.extend(configuration, config);
|
|
}
|
|
|
|
static getConfiguration() {
|
|
return configuration;
|
|
}
|
|
|
|
static getToolsGroupsMap() {
|
|
const toolsGroupsMap = {};
|
|
configuration.measurementTools.forEach(toolGroup => {
|
|
toolGroup.childTools.forEach(tool => (toolsGroupsMap[tool.id] = toolGroup.id));
|
|
});
|
|
return toolsGroupsMap;
|
|
}
|
|
|
|
constructor(timepointApi) {
|
|
if (timepointApi) {
|
|
this.timepointApi = timepointApi;
|
|
}
|
|
|
|
this.toolGroups = {};
|
|
this.tools = {};
|
|
this.toolsGroupsMap = MeasurementApi.getToolsGroupsMap();
|
|
this.changeObserver = new Tracker.Dependency();
|
|
|
|
configuration.measurementTools.forEach(toolGroup => {
|
|
const groupCollection = new Mongo.Collection(null);
|
|
groupCollection._debugName = toolGroup.name;
|
|
groupCollection.attachSchema(toolGroup.schema);
|
|
this.toolGroups[toolGroup.id] = groupCollection;
|
|
|
|
toolGroup.childTools.forEach(tool => {
|
|
const collection = new Mongo.Collection(null);
|
|
collection._debugName = tool.name;
|
|
collection.attachSchema(tool.schema);
|
|
this.tools[tool.id] = collection;
|
|
|
|
const addedHandler = measurement => {
|
|
let measurementNumber;
|
|
|
|
// Get the measurement number
|
|
const timepoint = this.timepointApi.timepoints.findOne({
|
|
studyInstanceUids: measurement.studyInstanceUid
|
|
});
|
|
|
|
// Preventing errors thrown when non-associated (standalone) study is opened...
|
|
// @TODO: Make sure this logic is correct.
|
|
if (!timepoint) {
|
|
return;
|
|
}
|
|
|
|
const emptyItem = groupCollection.findOne({
|
|
toolId: { $eq: null },
|
|
timepointId: timepoint.timepointId
|
|
});
|
|
|
|
if (emptyItem) {
|
|
measurementNumber = emptyItem.measurementNumber;
|
|
|
|
groupCollection.update({
|
|
timepointId: timepoint.timepointId,
|
|
measurementNumber
|
|
}, {
|
|
$set: {
|
|
toolId: tool.id,
|
|
toolItemId: measurement._id,
|
|
createdAt: measurement.createdAt
|
|
}
|
|
});
|
|
} else {
|
|
measurementNumber = groupCollection.find({
|
|
studyInstanceUid: { $in: timepoint.studyInstanceUids }
|
|
}).count() + 1;
|
|
}
|
|
|
|
measurement.measurementNumber = measurementNumber;
|
|
|
|
// Get the current location (if already defined)
|
|
let location;
|
|
const baselineTimepoint = timepointApi.baseline();
|
|
const baselineGroupEntry = groupCollection.findOne({
|
|
timepointId: baselineTimepoint.timepointId
|
|
});
|
|
if (baselineGroupEntry) {
|
|
const tool = this.tools[baselineGroupEntry.toolId];
|
|
const found = tool.findOne({ measurementNumber });
|
|
if (found) {
|
|
location = found.location;
|
|
}
|
|
}
|
|
|
|
// Set the timepoint ID, measurement number and location
|
|
collection.update(measurement._id, {
|
|
$set: {
|
|
timepointId: timepoint.timepointId,
|
|
measurementNumber,
|
|
location
|
|
}
|
|
});
|
|
|
|
if (!emptyItem) {
|
|
// Reflect the entry in the tool group collection
|
|
groupCollection.insert({
|
|
toolId: tool.id,
|
|
toolItemId: measurement._id,
|
|
timepointId: timepoint.timepointId,
|
|
studyInstanceUid: measurement.studyInstanceUid,
|
|
createdAt: measurement.createdAt,
|
|
measurementNumber
|
|
});
|
|
}
|
|
|
|
// Enable reactivity
|
|
this.changeObserver.changed();
|
|
};
|
|
|
|
const changedHandler = () => {
|
|
this.changeObserver.changed();
|
|
};
|
|
|
|
const removedHandler = measurement => {
|
|
const measurementNumber = measurement.measurementNumber;
|
|
|
|
groupCollection.update({
|
|
toolItemId: measurement._id
|
|
}, {
|
|
$set: {
|
|
toolId: null,
|
|
toolItemId: null
|
|
}
|
|
});
|
|
|
|
const nonEmptyItem = groupCollection.findOne({
|
|
measurementNumber,
|
|
toolId: { $not: null }
|
|
});
|
|
|
|
if (nonEmptyItem) {
|
|
return;
|
|
}
|
|
|
|
const groupItems = groupCollection.find({ measurementNumber }).fetch();
|
|
|
|
groupItems.forEach(groupItem => {
|
|
// Remove the record from the tools group collection too
|
|
groupCollection.remove({ _id: groupItem._id });
|
|
|
|
// Update the measurement numbers only if it is last item
|
|
const timepoint = this.timepointApi.timepoints.findOne({
|
|
timepointId: groupItem.timepointId
|
|
});
|
|
|
|
const filter = {
|
|
studyInstanceUid: { $in: timepoint.studyInstanceUids },
|
|
measurementNumber
|
|
};
|
|
|
|
const remainingItems = groupCollection.find(filter).count();
|
|
if (!remainingItems) {
|
|
filter.measurementNumber = { $gte: measurementNumber };
|
|
const operator = {
|
|
$inc: { measurementNumber: -1 }
|
|
};
|
|
const options = { multi: true };
|
|
groupCollection.update(filter, operator, options);
|
|
toolGroup.childTools.forEach(childTool => {
|
|
const collection = this.tools[childTool.id];
|
|
collection.update(filter, operator, options);
|
|
});
|
|
}
|
|
});
|
|
|
|
// Enable reactivity
|
|
this.changeObserver.changed();
|
|
};
|
|
|
|
collection.find().observe({
|
|
added: addedHandler,
|
|
changed: changedHandler,
|
|
removed: removedHandler
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
retrieveMeasurements(patientId, timepointIds) {
|
|
const retrievalFn = configuration.dataExchange.retrieve;
|
|
if (!_.isFunction(retrievalFn)) {
|
|
return;
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
retrievalFn(patientId, timepointIds).then(measurementData => {
|
|
|
|
OHIF.log.info('Measurement data retrieval');
|
|
OHIF.log.info(measurementData);
|
|
|
|
const toolsGroupsMap = MeasurementApi.getToolsGroupsMap();
|
|
const measurementsGroups = {};
|
|
|
|
Object.keys(measurementData).forEach(measurementTypeId => {
|
|
const measurements = measurementData[measurementTypeId];
|
|
|
|
measurements.forEach(measurement => {
|
|
const { toolType } = measurement;
|
|
if (toolType && this.tools[toolType]) {
|
|
delete measurement._id;
|
|
const toolGroup = toolsGroupsMap[toolType];
|
|
if (!measurementsGroups[toolGroup]) {
|
|
measurementsGroups[toolGroup] = [];
|
|
}
|
|
|
|
measurementsGroups[toolGroup].push(measurement);
|
|
}
|
|
});
|
|
});
|
|
|
|
Object.keys(measurementsGroups).forEach(groupKey => {
|
|
const group = measurementsGroups[groupKey];
|
|
group.sort((a, b) => {
|
|
if (a.measurementNumber > b.measurementNumber) {
|
|
return 1;
|
|
} else if (a.measurementNumber < b.measurementNumber) {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
});
|
|
|
|
group.forEach(m => this.tools[m.toolType].insert(m));
|
|
});
|
|
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
storeMeasurements() {
|
|
const storeFn = configuration.dataExchange.store;
|
|
if (!_.isFunction(storeFn)) {
|
|
return;
|
|
}
|
|
|
|
let measurementData = {};
|
|
configuration.measurementTools.forEach(toolGroup => {
|
|
toolGroup.childTools.forEach(tool => {
|
|
if (!measurementData[toolGroup.id]) {
|
|
measurementData[toolGroup.id] = [];
|
|
}
|
|
|
|
measurementData[toolGroup.id] = measurementData[toolGroup.id].concat(this.tools[tool.id].find().fetch());
|
|
});
|
|
});
|
|
|
|
const timepoints = this.timepointApi.all();
|
|
const timepointIds = timepoints.map(t => t.timepointId);
|
|
const patientId = timepoints[0].patientId;
|
|
const filter = {
|
|
patientId,
|
|
timepointId: {
|
|
$in: timepointIds
|
|
}
|
|
};
|
|
|
|
OHIF.log.info('Saving Measurements for timepoints:', timepoints);
|
|
return storeFn(measurementData, filter).then(() => {
|
|
OHIF.log.info('Measurement storage completed');
|
|
});
|
|
}
|
|
|
|
validateMeasurements() {
|
|
const validateFn = configuration.dataValidation.validateMeasurements;
|
|
if (validateFn && validateFn instanceof Function) {
|
|
validateFn();
|
|
}
|
|
}
|
|
|
|
syncMeasurementsAndToolData() {
|
|
configuration.measurementTools.forEach(toolGroup => {
|
|
toolGroup.childTools.forEach(tool => {
|
|
const measurements = this.tools[tool.id].find().fetch();
|
|
measurements.forEach(measurement => {
|
|
OHIF.measurements.syncMeasurementAndToolData(measurement);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
sortMeasurements(baselineTimepointId) {
|
|
const tools = configuration.measurementTools;
|
|
|
|
const includedTools = tools.filter(tool => {
|
|
return (tool.options && tool.options.caseProgress && tool.options.caseProgress.include);
|
|
});
|
|
|
|
// Update Measurement the displayed Measurements
|
|
includedTools.forEach(tool => {
|
|
const collection = this.tools[tool.id];
|
|
const measurements = collection.find().fetch();
|
|
measurements.forEach(measurement => {
|
|
OHIF.measurements.syncMeasurementAndToolData(measurement);
|
|
});
|
|
});
|
|
}
|
|
|
|
deleteMeasurements(measurementTypeId, filter) {
|
|
const groupCollection = this.toolGroups[measurementTypeId];
|
|
|
|
// Stop here if it is a temporary toolGroups
|
|
if (!groupCollection) return;
|
|
|
|
// Get the entries information before removing them
|
|
const groupItems = groupCollection.find(filter).fetch();
|
|
const entries = [];
|
|
groupItems.forEach(groupItem => {
|
|
if (!groupItem.toolId) {
|
|
return;
|
|
}
|
|
|
|
const collection = this.tools[groupItem.toolId];
|
|
entries.push(collection.findOne(groupItem.toolItemId));
|
|
collection.remove(groupItem.toolItemId);
|
|
});
|
|
|
|
// Stop here if no entries were found
|
|
if (!entries.length) {
|
|
return;
|
|
}
|
|
|
|
// If the filter doesn't have the measurement number, get it from the first entry
|
|
const measurementNumber = filter.measurementNumber || entries[0].measurementNumber;
|
|
|
|
// Synchronize the new data with cornerstone tools
|
|
const toolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState();
|
|
|
|
_.each(entries, entry => {
|
|
const imageId = OHIF.viewerbase.getImageIdForImagePath(entry.imagePath);
|
|
if (toolState[imageId]) {
|
|
const toolData = toolState[imageId][entry.toolType];
|
|
const measurementsData = toolData && toolData.data;
|
|
const measurementEntry = _.findWhere(measurementsData, {
|
|
_id: entry._id
|
|
});
|
|
|
|
if (measurementEntry) {
|
|
const index = measurementsData.indexOf(measurementEntry);
|
|
measurementsData.splice(index, 1);
|
|
}
|
|
}
|
|
});
|
|
|
|
cornerstoneTools.globalImageIdSpecificToolStateManager.restoreToolState(toolState);
|
|
|
|
// Synchronize the updated measurements with Cornerstone Tools
|
|
// toolData to make sure the displayed measurements show 'Target X' correctly
|
|
const syncFilter = _.clone(filter);
|
|
delete syncFilter.timepointId;
|
|
|
|
syncFilter.measurementNumber = {
|
|
$gt: measurementNumber - 1
|
|
};
|
|
|
|
const toolTypes = _.uniq(entries.map(entry => entry.toolType));
|
|
toolTypes.forEach(toolType => {
|
|
const collection = this.tools[toolType];
|
|
collection.find(syncFilter).forEach(measurement => {
|
|
OHIF.measurements.syncMeasurementAndToolData(measurement);
|
|
});
|
|
});
|
|
}
|
|
|
|
getMeasurementById(measurementId) {
|
|
let foundGroup;
|
|
_.find(this.toolGroups, toolGroup => {
|
|
foundGroup = toolGroup.findOne({ toolItemId: measurementId });
|
|
return !!foundGroup;
|
|
});
|
|
|
|
// Stop here if no group was found or if the record is a placeholder
|
|
if (!foundGroup || !foundGroup.toolId) {
|
|
return;
|
|
}
|
|
|
|
return this.tools[foundGroup.toolId].findOne(measurementId);
|
|
}
|
|
|
|
fetch(toolGroupId, selector, options) {
|
|
if (!this.toolGroups[toolGroupId]) {
|
|
throw 'MeasurementApi: No Collection with the id: ' + toolGroupId;
|
|
}
|
|
|
|
selector = selector || {};
|
|
options = options || {};
|
|
const result = [];
|
|
const items = this.toolGroups[toolGroupId].find(selector, options).fetch();
|
|
items.forEach(item => {
|
|
if (item.toolId) {
|
|
result.push(this.tools[item.toolId].findOne(item.toolItemId));
|
|
} else {
|
|
result.push({ measurementNumber: item.measurementNumber });
|
|
}
|
|
|
|
});
|
|
return result;
|
|
}
|
|
}
|
|
|
|
OHIF.measurements.MeasurementApi = MeasurementApi;
|