ohif-viewer/extensions/cornerstone/src/initMeasurementService.js
Alireza c9d3c08cb6
feat(cst): Add new command to get nearby annotation tools (#3327)
* feat(cst): Add new command to get nearby annotation tools
This commit adds a new command `getNearbyAnnotation` to the `commandsModule.ts` file that identifies nearby annotation tools excluding Crosshairs and ReferenceLines. It also removes an unused mapping to `Crosshairs` in `initMeasurementService.js` and changes the command run in `findNearbyToolData.ts` to `getNearbyAnnotation`.

* perf(cornerstone): Improve performance by optimizing tools

Removes unnecessary imports to improve app's performance. Changes how annotations are detected on the `commandsModule.js` file by fetching the `isAnnotation` property of the tool instance if available. Updates `CrosshairsTool` and `ReferenceLinesTool` to not be annotations anymore. Adjusts the `localhost` URL ports for the `dcm4chee-arc` service interface on `local_dcm4chee.js` to allow for more efficient server communication.

* feat(cornerstone): Add CornerstoneServices type and ToolGroupService.getToolGroup to get tool group by ID or active viewport

This commit introduces the new CornerstoneServices interface to the code and adds the ToolGroupService.getToolGroup method, which can retrieve a specific tool group by ID or from the active viewport. The older _getToolGroup method has also been removed from commandsModule. When no tool group ID is provided, this method now retrieves the tool group from the currently active viewport with the help of getActiveViewportEnabledElement. Additionally, the required reference to @ohif/core has been removed.
2023-04-28 10:14:14 -04:00

420 lines
12 KiB
JavaScript

import { eventTarget } from '@cornerstonejs/core';
import { Enums, annotation } from '@cornerstonejs/tools';
import { DicomMetadataStore } from '@ohif/core';
import { toolNames } from './initCornerstoneTools';
import { onCompletedCalibrationLine } from './tools/CalibrationLineTool';
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
const { removeAnnotation } = annotation.state;
const csToolsEvents = Enums.Events;
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
const initMeasurementService = (
measurementService,
displaySetService,
cornerstoneViewportService
) => {
/* Initialization */
const {
Length,
Bidirectional,
EllipticalROI,
CircleROI,
ArrowAnnotate,
Angle,
CobbAngle,
RectangleROI,
PlanarFreehandROI,
} = measurementServiceMappingsFactory(
measurementService,
displaySetService,
cornerstoneViewportService
);
const csTools3DVer1MeasurementSource = measurementService.createSource(
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
);
/* Mappings */
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'Length',
Length.matchingCriteria,
Length.toAnnotation,
Length.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'Bidirectional',
Bidirectional.matchingCriteria,
Bidirectional.toAnnotation,
Bidirectional.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'EllipticalROI',
EllipticalROI.matchingCriteria,
EllipticalROI.toAnnotation,
EllipticalROI.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'CircleROI',
CircleROI.matchingCriteria,
CircleROI.toAnnotation,
CircleROI.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'ArrowAnnotate',
ArrowAnnotate.matchingCriteria,
ArrowAnnotate.toAnnotation,
ArrowAnnotate.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'CobbAngle',
CobbAngle.matchingCriteria,
CobbAngle.toAnnotation,
CobbAngle.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'Angle',
Angle.matchingCriteria,
Angle.toAnnotation,
Angle.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'RectangleROI',
RectangleROI.matchingCriteria,
RectangleROI.toAnnotation,
RectangleROI.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'PlanarFreehandROI',
PlanarFreehandROI.matchingCriteria,
PlanarFreehandROI.toAnnotation,
PlanarFreehandROI.toMeasurement
);
// On the UI side, the Calibration Line tool will work almost the same as the
// Length tool
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'CalibrationLine',
Length.matchingCriteria,
Length.toAnnotation,
Length.toMeasurement
);
return csTools3DVer1MeasurementSource;
};
const connectToolsToMeasurementService = servicesManager => {
const {
measurementService,
displaySetService,
cornerstoneViewportService,
} = servicesManager.services;
const csTools3DVer1MeasurementSource = initMeasurementService(
measurementService,
displaySetService,
cornerstoneViewportService
);
connectMeasurementServiceToTools(
measurementService,
cornerstoneViewportService,
csTools3DVer1MeasurementSource
);
const { annotationToMeasurement, remove } = csTools3DVer1MeasurementSource;
//
function addMeasurement(csToolsEvent) {
try {
const annotationAddedEventDetail = csToolsEvent.detail;
const {
annotation: { metadata, annotationUID },
} = annotationAddedEventDetail;
const { toolName } = metadata;
if (
csToolsEvent.type === completedEvt &&
toolName === toolNames.CalibrationLine
) {
// show modal to input the measurement (mm)
onCompletedCalibrationLine(servicesManager, csToolsEvent)
.then(
() => {
console.log('calibration applied');
},
() => true
)
.finally(() => {
// we don't need the calibration line lingering around, remove the
// annotation from the display
removeAnnotation(annotationUID);
removeMeasurement(csToolsEvent);
// this will ensure redrawing of annotations
cornerstoneViewportService.resize();
});
} else {
// To force the measurementUID be the same as the annotationUID
// Todo: this should be changed when a measurement can include multiple annotations
// in the future
annotationAddedEventDetail.uid = annotationUID;
annotationToMeasurement(toolName, annotationAddedEventDetail);
}
} catch (error) {
console.warn('Failed to update measurement:', error);
}
}
function updateMeasurement(csToolsEvent) {
try {
const annotationModifiedEventDetail = csToolsEvent.detail;
const {
annotation: { metadata, annotationUID },
} = annotationModifiedEventDetail;
// If the measurement hasn't been added, don't modify it
const measurement = measurementService.getMeasurement(annotationUID);
if (!measurement) {
return;
}
const { toolName } = metadata;
annotationModifiedEventDetail.uid = annotationUID;
// Passing true to indicate this is an update and NOT a annotation (start) completion.
annotationToMeasurement(toolName, annotationModifiedEventDetail, true);
} catch (error) {
console.warn('Failed to update measurement:', error);
}
}
function selectMeasurement(csToolsEvent) {
try {
const annotationSelectionEventDetail = csToolsEvent.detail;
const {
added: addedSelectedAnnotationUIDs,
removed: removedSelectedAnnotationUIDs,
} = annotationSelectionEventDetail;
if (removedSelectedAnnotationUIDs) {
removedSelectedAnnotationUIDs.forEach(annotationUID =>
measurementService.setMeasurementSelected(annotationUID, false)
);
}
if (addedSelectedAnnotationUIDs) {
addedSelectedAnnotationUIDs.forEach(annotationUID =>
measurementService.setMeasurementSelected(annotationUID, true)
);
}
} catch (error) {
console.warn('Failed to select and unselect measurements:', error);
}
}
/**
* When csTools fires a removed event, remove the same measurement
* from the measurement service
*
* @param {*} csToolsEvent
*/
function removeMeasurement(csToolsEvent) {
try {
try {
const annotationRemovedEventDetail = csToolsEvent.detail;
const {
annotation: { annotationUID },
} = annotationRemovedEventDetail;
const measurement = measurementService.getMeasurement(annotationUID);
if (measurement) {
console.log('~~ removeEvt', csToolsEvent);
remove(annotationUID, annotationRemovedEventDetail);
}
} catch (error) {
console.warn('Failed to update measurement:', error);
}
} catch (error) {
console.warn('Failed to remove measurement:', error);
}
}
// on display sets added, check if there are any measurements in measurement service that need to be
// put into cornerstone tools
const addedEvt = csToolsEvents.ANNOTATION_ADDED;
const completedEvt = csToolsEvents.ANNOTATION_COMPLETED;
const updatedEvt = csToolsEvents.ANNOTATION_MODIFIED;
const removedEvt = csToolsEvents.ANNOTATION_REMOVED;
const selectionEvt = csToolsEvents.ANNOTATION_SELECTION_CHANGE;
eventTarget.addEventListener(addedEvt, addMeasurement);
eventTarget.addEventListener(completedEvt, addMeasurement);
eventTarget.addEventListener(updatedEvt, updateMeasurement);
eventTarget.addEventListener(removedEvt, removeMeasurement);
eventTarget.addEventListener(selectionEvt, selectMeasurement);
return csTools3DVer1MeasurementSource;
};
const connectMeasurementServiceToTools = (
measurementService,
cornerstoneViewportService,
measurementSource
) => {
const {
MEASUREMENT_REMOVED,
MEASUREMENTS_CLEARED,
MEASUREMENT_UPDATED,
RAW_MEASUREMENT_ADDED,
} = measurementService.EVENTS;
const csTools3DVer1MeasurementSource = measurementService.getSource(
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
);
measurementService.subscribe(MEASUREMENTS_CLEARED, ({ measurements }) => {
if (!Object.keys(measurements).length) {
return;
}
for (const measurement of Object.values(measurements)) {
const { uid, source } = measurement;
if (source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) {
continue;
}
removeAnnotation(uid);
}
});
measurementService.subscribe(
MEASUREMENT_UPDATED,
({ source, measurement, notYetUpdatedAtSource }) => {
if (source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) {
return;
}
if (notYetUpdatedAtSource === false) {
// This event was fired by cornerstone telling the measurement service to sync.
// Already in sync.
return;
}
const { uid, label } = measurement;
const sourceAnnotation = annotation.state.getAnnotation(uid);
const { data, metadata } = sourceAnnotation;
if (!data) {
return;
}
if (data.label !== label) {
data.label = label;
}
if (metadata.toolName === 'ArrowAnnotate') {
data.text = label;
}
// Todo: trigger render for annotation
}
);
measurementService.subscribe(
RAW_MEASUREMENT_ADDED,
({ source, measurement, data, dataSource }) => {
if (source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) {
return;
}
const {
referenceSeriesUID,
referenceStudyUID,
SOPInstanceUID,
} = measurement;
const instance = DicomMetadataStore.getInstance(
referenceStudyUID,
referenceSeriesUID,
SOPInstanceUID
);
let imageId;
let frameNumber = 1;
if (measurement?.metadata?.referencedImageId) {
imageId = measurement.metadata.referencedImageId;
frameNumber = getSOPInstanceAttributes(
measurement.metadata.referencedImageId
).frameNumber;
} else {
imageId = dataSource.getImageIdsForInstance({ instance });
}
const annotationManager = annotation.state.getAnnotationManager();
annotationManager.addAnnotation({
annotationUID: measurement.uid,
highlighted: false,
isLocked: false,
invalidated: false,
metadata: {
toolName: measurement.toolName,
FrameOfReferenceUID: measurement.FrameOfReferenceUID,
referencedImageId: imageId,
},
data: {
text: data.annotation.data.text,
handles: { ...data.annotation.data.handles },
cachedStats: { ...data.annotation.data.cachedStats },
label: data.annotation.data.label,
frameNumber: frameNumber,
},
});
}
);
measurementService.subscribe(
MEASUREMENT_REMOVED,
({ source, measurement: removedMeasurementId }) => {
if (source?.name && source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) {
return;
}
removeAnnotation(removedMeasurementId);
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
// Note: We could do a better job by triggering the render on the
// viewport itself, but the removeAnnotation does not include that info...
renderingEngine.render();
}
);
};
export {
initMeasurementService,
connectToolsToMeasurementService,
connectMeasurementServiceToTools,
};