[OHIF-193 + OHIF-313] (#1955)

* create context menu

* OHIF-193: Add context menu integration with measurement service

* OHIF-193: Update comments

* OHIF-CR Update: Add requested updates

* CR Update: Update measurement service remove function

* CR Update: Update casing

* CR Updates

* Update cornerstone measurments when the label is edited in the measurement service.

* Implement click outside behavior

* Address reviewer comments.

Co-authored-by: Rodrigo Antinarelli <rodrigoantinarelli@gmail.com>
Co-authored-by: igoroctaviano <igoroctaviano@gmail.com>
This commit is contained in:
authored and GitHub committed 2020-08-05 14:56:02 +02:00
1 parent 39e68f3193
commit 401a2e38c8
20 files changed
+575 -196

No files matched your search

+2 -2
View File
@@ -30,8 +30,8 @@ export default {
* @param {object} [configuration={}]
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
*/
preRegistration({ servicesManager, configuration = {} }) {
init({ servicesManager, configuration });
preRegistration({ servicesManager, commandsManager, configuration = {} }) {
init({ servicesManager, commandsManager, configuration });
},
getViewportModule({ commandsManager }) {
const ExtendedOHIFCornerstoneViewport = props => {
+271 -83
View File
@@ -1,32 +1,239 @@
import React from 'react';
import OHIF from '@ohif/core';
import { Dialog, Input } from '@ohif/ui';
import cornerstone from 'cornerstone-core';
import { Input, Dialog, ContextMenuMeasurements } from '@ohif/ui';
import cs from 'cornerstone-core';
import csTools from 'cornerstone-tools';
import merge from 'lodash.merge';
import initCornerstoneTools from './initCornerstoneTools.js';
import cornerstoneTools from 'cornerstone-tools';
import initWADOImageLoader from './initWADOImageLoader.js';
import './initWADOImageLoader.js';
import getCornerstoneMeasurementById from './utils/getCornerstoneMeasurementById';
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
//
import { setEnabledElement } from './state';
// TODO -> Global "context menu open state", or lots of expensive searches on drag?
let CONTEXT_MENU_OPEN = false;
const { globalImageIdSpecificToolStateManager } = csTools;
const TOOL_TYPES_WITH_CONTEXT_MENU = [
'Angle',
'ArrowAnnotate',
'Bidirectional',
'Length',
'FreehandMouse',
'EllipticalRoi',
'CircleRoi',
'RectangleRoi',
];
const _refreshViewports = () =>
cs.getEnabledElements().forEach(({ element }) => cs.updateImage(element));
/**
*
* @param {Object} servicesManager
* @param {Object} configuration
* @param {Object|Array} configuration.csToolsConfig
*/
export default function init({ servicesManager, configuration }) {
export default function init({
servicesManager,
commandsManager,
configuration,
}) {
const {
UIDialogService,
MeasurementService,
DisplaySetService,
} = servicesManager.services;
const callInputDialog = (data, event, callback) => {
/* Measurement Service */
const measurementServiceSource = _connectToolsToMeasurementService(
MeasurementService,
DisplaySetService
);
const onRightClick = event => {
if (!UIDialogService) {
console.warn('Unable to show dialog; no UI Dialog Service available.');
return;
}
const onGetMenuItems = defaultMenuItems => {
const { element, currentPoints } = event.detail;
const nearbyToolData = commandsManager.runCommand('getNearbyToolData', {
element,
canvasCoordinates: currentPoints.canvas,
availableToolTypes: TOOL_TYPES_WITH_CONTEXT_MENU,
});
let menuItems = [];
if (nearbyToolData) {
defaultMenuItems.forEach(item => {
item.value = nearbyToolData;
menuItems.push(item);
});
}
return menuItems;
};
CONTEXT_MENU_OPEN = true;
UIDialogService.dismiss({ id: 'context-menu' });
UIDialogService.create({
id: 'context-menu',
isDraggable: false,
preservePosition: false,
defaultPosition: _getDefaultPosition(event.detail),
content: ContextMenuMeasurements,
onClickOutside: () => {
UIDialogService.dismiss({ id: 'context-menu' });
CONTEXT_MENU_OPEN = false;
},
contentProps: {
onGetMenuItems,
eventData: event.detail,
onDelete: item => {
const { tool: measurementData, toolType } = item.value;
measurementServiceSource.remove(measurementData.id);
_refreshViewports();
CONTEXT_MENU_OPEN = false;
},
onClose: () => {
CONTEXT_MENU_OPEN = false;
UIDialogService.dismiss({ id: 'context-menu' });
},
onSetLabel: item => {
const { tool: measurementData } = item.value;
const measurement = MeasurementService.getMeasurement(
measurementData.id
);
callInputDialog(
measurement,
(label, actionId) => {
if (actionId === 'cancel') {
return;
}
const updatedMeasurement = Object.assign({}, measurement, {
label,
});
MeasurementService.update(
updatedMeasurement.id,
updatedMeasurement,
true
);
},
false
);
CONTEXT_MENU_OPEN = false;
},
},
});
};
const onTouchPress = event => {
if (!UIDialogService) {
console.warn('Unable to show dialog; no UI Dialog Service available.');
return;
}
UIDialogService.create({
eventData: event.detail,
content: ContextMenuMeasurements,
contentProps: { isTouchEvent: true },
});
};
const resetContextMenu = () => {
if (!UIDialogService) {
console.warn('Unable to show dialog; no UI Dialog Service available.');
return;
}
CONTEXT_MENU_OPEN = false;
UIDialogService.dismiss({ id: 'context-menu' });
};
/*
* Because click gives us the native "mouse up", buttons will always be `0`
* Need to fallback to event.which;
*
*/
const contextMenuHandleClick = evt => {
const mouseUpEvent = evt.detail.event;
const isRightClick = mouseUpEvent.which === 3;
const clickMethodHandler = isRightClick ? onRightClick : resetContextMenu;
clickMethodHandler(evt);
};
const cancelContextMenuIfOpen = evt => {
if (CONTEXT_MENU_OPEN) {
resetContextMenu();
}
};
function elementEnabledHandler(evt) {
const element = evt.detail.element;
element.addEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
element.addEventListener(
csTools.EVENTS.MOUSE_CLICK,
contextMenuHandleClick
);
element.addEventListener(cs.EVENTS.NEW_IMAGE, cancelContextMenuIfOpen);
}
function elementDisabledHandler(evt) {
const element = evt.detail.element;
element.removeEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
element.removeEventListener(
csTools.EVENTS.MOUSE_CLICK,
contextMenuHandleClick
);
element.removeEventListener(cs.EVENTS.NEW_IMAGE, cancelContextMenuIfOpen);
}
/**
*
* @param {*} data
* @param {*} event
* @param {*} callback
* @param {*} isArrowAnnotateInputDialog
*/
const callInputDialog = (
data,
callback,
isArrowAnnotateInputDialog = true
) => {
const dialogId = 'enter-annotation';
const label = data
? isArrowAnnotateInputDialog
? data.text
: data.label
: '';
const onSubmitHandler = ({ action, value }) => {
switch (action.id) {
case 'save':
callback(value.label, action.id);
break;
case 'cancel':
callback('', action.id);
break;
}
UIDialogService.dismiss({ id: dialogId });
};
if (UIDialogService) {
let dialogId = UIDialogService.create({
UIDialogService.create({
id: dialogId,
centralize: true,
isDraggable: false,
content: Dialog,
@@ -34,34 +241,26 @@ export default function init({ servicesManager, configuration }) {
showOverlay: true,
contentProps: {
title: 'Enter your annotation',
value: { label: data ? data.text : '' },
value: { label },
noCloseButton: true,
onClose: () => UIDialogService.dismiss({ id: dialogId }),
actions: [
{ id: 'cancel', text: 'Cancel', type: 'secondary' },
{ id: 'save', text: 'Save', type: 'primary' },
],
onSubmit: ({ action, value }) => {
switch (action.id) {
case 'save':
callback(value.label);
break;
case 'cancel':
callback();
break;
}
UIDialogService.dismiss({ id: dialogId });
},
onSubmit: onSubmitHandler,
body: ({ value, setValue }) => {
const onChangeHandler = event => {
event.persist();
setValue(value => ({ ...value, label: event.target.value }));
};
const onKeyPressHandler = event => {
if (event.key === 'Enter') {
onSubmitHandler({ value, action: { id: 'save' } });
}
};
return (
<div className="p-4 bg-primary-dark">
<Input
@@ -84,10 +283,7 @@ export default function init({ servicesManager, configuration }) {
const { csToolsConfig } = configuration;
const metadataProvider = OHIF.cornerstone.metadataProvider;
cornerstone.metaData.addProvider(
metadataProvider.get.bind(metadataProvider),
9999
);
cs.metaData.addProvider(metadataProvider.get.bind(metadataProvider), 9999);
// ~~
const defaultCsToolsConfig = csToolsConfig || {
@@ -143,17 +339,14 @@ export default function init({ servicesManager, configuration }) {
tools.push(...toolsGroupedByType[toolsGroup])
);
/* Measurement Service */
_connectToolsToMeasurementService(MeasurementService, DisplaySetService);
/* Add extension tools configuration here. */
const internalToolsConfig = {
ArrowAnnotate: {
configuration: {
getTextCallback: (callback, eventDetails) =>
callInputDialog(null, eventDetails, callback),
callInputDialog(null, callback),
changeTextCallback: (data, eventDetails, callback) =>
callInputDialog(data, eventDetails, callback),
callInputDialog(data, callback),
allowEmptyLabel: true,
},
},
@@ -229,6 +422,12 @@ export default function init({ servicesManager, configuration }) {
csTools.setToolActive('PanMultiTouch', { pointers: 2 }); // TODO: Better error if no options
csTools.setToolActive('ZoomTouchPinch', {});
csTools.setToolEnabled('Overlay', {});
cs.events.addEventListener(cs.EVENTS.ELEMENT_ENABLED, elementEnabledHandler);
cs.events.addEventListener(
cs.EVENTS.ELEMENT_DISABLED,
elementDisabledHandler
);
}
const _initMeasurementService = (MeasurementService, DisplaySetService) => {
@@ -293,12 +492,11 @@ const _connectToolsToMeasurementService = (
csToolsVer4MeasurementSource
);
const { addOrUpdate, remove } = csToolsVer4MeasurementSource;
const elementEnabledEvt = cornerstone.EVENTS.ELEMENT_ENABLED;
const elementEnabledEvt = cs.EVENTS.ELEMENT_ENABLED;
/* Measurement Service Events */
cornerstone.events.addEventListener(elementEnabledEvt, evt => {
cs.events.addEventListener(elementEnabledEvt, evt => {
// TODO: Debounced update of measurements that are modified
function addMeasurement(csToolsEvent) {
console.log('CSTOOLS::addOrUpdate', csToolsEvent, csToolsEvent.detail);
@@ -322,13 +520,13 @@ const _connectToolsToMeasurementService = (
if (!csToolsEvent.detail.measurementData.id) {
return;
}
const evtDetail = csToolsEvent.detail;
const { toolName, toolType, measurementData } = evtDetail;
const csToolName = toolName || measurementData.toolType || toolType;
evtDetail.id = csToolsEvent.detail.measurementData.id;
addOrUpdate(csToolName, evtDetail);
//
} catch (error) {
console.warn('Failed to update measurement:', error);
}
@@ -351,14 +549,42 @@ const _connectToolsToMeasurementService = (
}
}
const { MEASUREMENTS_CLEARED } = MeasurementService.EVENTS;
const {
MEASUREMENTS_CLEARED,
MEASUREMENT_UPDATED,
} = MeasurementService.EVENTS;
MeasurementService.subscribe(MEASUREMENTS_CLEARED, () => {
cornerstoneTools.globalImageIdSpecificToolStateManager.restoreToolState(
{}
);
globalImageIdSpecificToolStateManager.restoreToolState({});
_refreshViewports();
});
MeasurementService.subscribe(
MEASUREMENT_UPDATED,
({ source, measurement, notYetUpdatedAtSource }) => {
const { id, label } = measurement;
if (
source.name == 'CornerstoneTools' &&
notYetUpdatedAtSource === false
) {
// This event was fired by cornerstone telling the measurement service to sync. Already in sync.
return;
}
const cornerstoneMeasurement = getCornerstoneMeasurementById(id);
if (cornerstoneMeasurement) {
cornerstoneMeasurement.label = label;
if (cornerstoneMeasurement.hasOwnProperty('text')) {
// Deal with the weird case of ArrowAnnotate.
cornerstoneMeasurement.text = label;
}
_refreshViewports();
}
}
);
const enabledElement = evt.detail.element;
const completedEvt = csTools.EVENTS.MEASUREMENT_COMPLETED;
const updatedEvt = csTools.EVENTS.MEASUREMENT_MODIFIED;
@@ -368,25 +594,17 @@ const _connectToolsToMeasurementService = (
enabledElement.addEventListener(updatedEvt, updateMeasurement);
enabledElement.addEventListener(removedEvt, removeMeasurement);
});
return csToolsVer4MeasurementSource;
};
const _connectMeasurementServiceToTools = (
MeasurementService,
measurementSource
) => {
const {
MEASUREMENTS_CLEARED,
MEASUREMENT_REMOVED,
} = MeasurementService.EVENTS;
const { MEASUREMENT_REMOVED } = MeasurementService.EVENTS;
const sourceId = measurementSource.id;
MeasurementService.subscribe(MEASUREMENTS_CLEARED, () => {
cornerstoneTools.globalImageIdSpecificToolStateManager.restoreToolState({});
cornerstone.getEnabledElements().forEach(enabledElement => {
cornerstone.updateImage(enabledElement.element);
});
});
// TODO: This is an unsafe delete
// Cornerstone-tools should probably expose a more generic "delete by id"
// And have toolState managers expose a method to find any of their toolState by ID
@@ -401,7 +619,7 @@ const _connectMeasurementServiceToTools = (
MEASUREMENT_REMOVED,
({ source, measurement: removedMeasurementId }) => {
// THIS POINTS TO ORIGINAL; Not a copy
const imageIdSpecificToolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState();
const imageIdSpecificToolState = globalImageIdSpecificToolStateManager.saveToolState();
// ImageId -->
Object.keys(imageIdSpecificToolState).forEach(imageId => {
@@ -425,37 +643,7 @@ const _connectMeasurementServiceToTools = (
);
};
// const {
// MEASUREMENT_ADDED,
// MEASUREMENT_UPDATED,
// } = MeasurementService.EVENTS;
// MeasurementService.subscribe(
// MEASUREMENT_ADDED,
// ({ source, measurement }) => {
// if (![sourceId].includes(source.id)) {
// const annotation = getAnnotation('Length', measurement.id);
// console.log(
// 'Measurement Service [Cornerstone]: Measurement added',
// measurement
// );
// console.log('Mapped annotation:', annotation);
// }
// }
// );
// MeasurementService.subscribe(
// MEASUREMENT_UPDATED,
// ({ source, measurement }) => {
// if (![sourceId].includes(source.id)) {
// const annotation = getAnnotation('Length', measurement.id);
// console.log(
// 'Measurement Service [Cornerstone]: Measurement updated',
// measurement
// );
// console.log('Mapped annotation:', annotation);
// }
// }
// );
const _getDefaultPosition = event => ({
x: (event && event.currentPoints.client.x) || 0,
y: (event && event.currentPoints.client.y) || 0,
});
@@ -0,0 +1,31 @@
import cornerstoneTools from 'cornerstone-tools';
const { globalImageIdSpecificToolStateManager } = cornerstoneTools;
export default function getCornerstoneMeasurementById(id) {
const globalToolState = globalImageIdSpecificToolStateManager.saveToolState();
const imageIds = Object.keys(globalToolState);
for (let i = 0; i < imageIds.length; i++) {
const imageId = imageIds[i];
const imageIdSpecificToolState = globalToolState[imageId];
const toolTypes = Object.keys(imageIdSpecificToolState);
for (let j = 0; j < toolTypes.length; j++) {
const toolType = toolTypes[j];
const toolData = imageIdSpecificToolState[toolType].data;
if (toolData) {
for (let k = 0; k < toolData.length; k++) {
const toolDataK = toolData[k];
if (toolDataK.id === id) {
return toolDataK;
}
}
}
}
}
}
@@ -1,11 +1,10 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getPointsFromHandles from './utils/getPointsFromHandles';
import getHandlesFromPoints from './utils/getHandlesFromPoints';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
const ArrowAnnotate = {
toAnnotation: (measurement, definition) => {
// TODO -> Implement when this is needed.
},
toAnnotation: (measurement, definition) => {},
toMeasurement: (
csToolsAnnotation,
DisplaySetService,
@@ -40,7 +39,7 @@ const ArrowAnnotate = {
return {
id: measurementData.id,
SOPInstanceUID: SOPInstanceUID,
SOPInstanceUID,
FrameOfReferenceUID,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
@@ -1,10 +1,9 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import getHandlesFromPoints from './utils/getHandlesFromPoints';
const Bidirectional = {
toAnnotation: (measurement, definition) => {
// TODO -> Implement when this is needed.
},
toAnnotation: (measurement, definition) => {},
toMeasurement: (
csToolsAnnotation,
DisplaySetService,
@@ -41,12 +40,12 @@ const Bidirectional = {
return {
id: measurementData.id,
SOPInstanceUID: SOPInstanceUID,
SOPInstanceUID,
FrameOfReferenceUID,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: measurementData.text,
label: measurementData.label,
description: measurementData.description,
unit: measurementData.unit,
shortestDiameter: measurementData.shortestDiameter,
@@ -1,10 +1,9 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import getHandlesFromPoints from './utils/getHandlesFromPoints';
const EllipticalRoi = {
toAnnotation: (measurement, definition) => {
// TODO -> Implement when this is needed.
},
toAnnotation: (measurement, definition) => {},
toMeasurement: (
csToolsAnnotation,
DisplaySetService,
@@ -63,12 +62,12 @@ const EllipticalRoi = {
return {
id: measurementData.id,
SOPInstanceUID: SOPInstanceUID,
SOPInstanceUID,
FrameOfReferenceUID,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: measurementData.text,
label: measurementData.label,
description: measurementData.description,
unit: measurementData.unit,
area:
@@ -4,32 +4,7 @@ import getPointsFromHandles from './utils/getPointsFromHandles';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
const Length = {
toAnnotation: (measurement, definition) => {
const {
id,
label,
description,
points,
unit,
SOPInstanceUID,
FrameOfReferenceUID,
referenceSeriesUID,
} = measurement;
return {
toolName: definition,
measurementData: {
sopInstanceUid: SOPInstanceUID,
frameOfReferenceUID: FrameOfReferenceUID,
SeriesInstanceUID: referenceSeriesUID,
unit,
text: label,
description,
handles: getHandlesFromPoints(points),
id,
},
};
},
toAnnotation: (measurement, definition) => {},
/**
* Maps cornerstone annotation event data to measurement service format.
@@ -68,12 +43,12 @@ const Length = {
return {
id: measurementData.id,
SOPInstanceUID: SOPInstanceUID,
SOPInstanceUID,
FrameOfReferenceUID,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: measurementData.text,
label: measurementData.label,
description: measurementData.description,
unit: measurementData.unit,
length: measurementData.length,
@@ -44,8 +44,8 @@ describe('measurementServiceMappings.js', () => {
toolName: definition,
measurementData: {
_measurementServiceId: 1,
sopInstanceUid: '123',
frameOfReferenceUID: '123',
SOPInstanceUID: '123',
FrameOfReferenceUID: '123',
SeriesInstanceUID: '123',
handles,
text: 'Test',
@@ -1,4 +1,13 @@
export default function getHandlesFromPoints(points) {
if (points.longAxis && points.shortAxis) {
const handles = {};
handles.start = points.longAxis[0];
handles.end = points.longAxis[1];
handles.perpendicularStart = points.longAxis[0];
handles.perpendicularEnd = points.longAxis[1];
return handles;
}
return points
.map((p, i) => (i % 10 === 0 ? { start: p } : { end: p }))
.reduce((obj, item) => Object.assign(obj, { ...item }), {});