* 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.
420 lines
12 KiB
JavaScript
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,
|
|
};
|