diff --git a/extensions/cornerstone/src/index.js b/extensions/cornerstone/src/index.js
index 4885d0591..0d0ec725f 100644
--- a/extensions/cornerstone/src/index.js
+++ b/extensions/cornerstone/src/index.js
@@ -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 => {
diff --git a/extensions/cornerstone/src/init.js b/extensions/cornerstone/src/init.js
index 2d1470bdc..a56a5c4c6 100644
--- a/extensions/cornerstone/src/init.js
+++ b/extensions/cornerstone/src/init.js
@@ -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 (
- 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,
+});
diff --git a/extensions/cornerstone/src/utils/getCornerstoneMeasurementById.js b/extensions/cornerstone/src/utils/getCornerstoneMeasurementById.js
new file mode 100644
index 000000000..61f3003a0
--- /dev/null
+++ b/extensions/cornerstone/src/utils/getCornerstoneMeasurementById.js
@@ -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;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js
index 8a081e2e4..89b4184f7 100644
--- a/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js
+++ b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js
@@ -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,
diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.js b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.js
index e31cc7849..34eeb21ff 100644
--- a/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.js
+++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.js
@@ -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,
diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js
index 19c2803b5..5ecf0b19a 100644
--- a/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js
+++ b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js
@@ -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:
diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js
index 777e98a0a..273e4e44b 100644
--- a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js
+++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js
@@ -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,
diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.test.js b/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.test.js
index 067782d0e..f990e5258 100644
--- a/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.test.js
+++ b/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.test.js
@@ -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',
diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getHandlesFromPoints.js b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getHandlesFromPoints.js
index 1440405bc..6d381941b 100644
--- a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getHandlesFromPoints.js
+++ b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getHandlesFromPoints.js
@@ -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 }), {});
diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js
index 614231ff4..6b22285fd 100644
--- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js
+++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js
@@ -162,19 +162,24 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
const onMeasurementItemEditHandler = ({ id }) => {
const measurement = MeasurementService.getMeasurement(id);
- let dialogId;
const onSubmitHandler = ({ action, value }) => {
switch (action.id) {
case 'save': {
- MeasurementService.update(id, {
- ...measurement,
- ...value,
- });
+ MeasurementService.update(
+ id,
+ {
+ ...measurement,
+ ...value,
+ },
+ true
+ );
}
}
- UIDialogService.dismiss({ id: dialogId });
+ UIDialogService.dismiss({ id: 'enter-annotation' });
};
- dialogId = UIDialogService.create({
+
+ UIDialogService.create({
+ id: 'enter-annotation',
centralize: true,
isDraggable: false,
useLastPosition: false,
diff --git a/platform/core/src/services/MeasurementService/MeasurementService.js b/platform/core/src/services/MeasurementService/MeasurementService.js
index 82baa8e8c..0618cef1f 100644
--- a/platform/core/src/services/MeasurementService/MeasurementService.js
+++ b/platform/core/src/services/MeasurementService/MeasurementService.js
@@ -16,7 +16,7 @@ import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
*
* @typedef {Object} Measurement
* @property {number} id -
- * @property {string} sopInstanceUid -
+ * @property {string} SOPInstanceUID -
* @property {string} FrameOfReferenceUID -
* @property {string} referenceSeriesUID -
* @property {string} label -
@@ -50,6 +50,7 @@ const MEASUREMENT_SCHEMA_KEYS = [
const EVENTS = {
MEASUREMENT_UPDATED: 'event::measurement_updated',
+ INTERNAL_MEASUREMENT_UPDATED: 'event:internal_measurement_updated',
MEASUREMENT_ADDED: 'event::measurement_added',
MEASUREMENT_REMOVED: 'event::measurement_removed',
MEASUREMENTS_CLEARED: 'event::measurements_cleared',
@@ -146,7 +147,7 @@ class MeasurementService {
return this.addOrUpdate(source, definition, measurement);
};
source.remove = id => {
- return this.remove(source, id);
+ return this.remove(id, source);
};
source.getAnnotation = (definition, measurementId) => {
return this.getAnnotation(source, definition, measurementId);
@@ -216,11 +217,6 @@ class MeasurementService {
return;
}
- if (!toSourceSchema) {
- log.warn('Source mapping function not provided. Exiting early.');
- return;
- }
-
if (!toMeasurementSchema) {
log.warn('Measurement mapping function not provided. Exiting early.');
return;
@@ -269,9 +265,9 @@ class MeasurementService {
measurementId,
definition
);
+ const measurement = this.getMeasurement(measurementId);
if (mapping) return mapping.toSourceSchema(measurement, definition);
- const measurement = this.getMeasurement(measurementId);
const matchingMapping = this._getMatchingMapping(
source,
definition,
@@ -285,21 +281,28 @@ class MeasurementService {
}
}
- update(id, measurement) {
+ update(id, measurement, notYetUpdatedAtSource = false) {
if (this.measurements[id]) {
const updatedMeasurement = {
...measurement,
modifiedTimestamp: Math.floor(Date.now() / 1000),
};
- log.info(`Updating measurement...`, updatedMeasurement);
+ log.info(
+ `Updating internal measurement representation...`,
+ updatedMeasurement
+ );
this.measurements[id] = updatedMeasurement;
this._broadcastChange(
+ // Add an internal flag to say the measurement has not yet been updated at source.
this.EVENTS.MEASUREMENT_UPDATED,
- measurement.source,
- updatedMeasurement
+ {
+ source: measurement.source,
+ measurement: updatedMeasurement,
+ notYetUpdatedAtSource,
+ }
);
return updatedMeasurement.id;
@@ -375,19 +378,17 @@ class MeasurementService {
newMeasurement
);
this.measurements[internalId] = newMeasurement;
- this._broadcastChange(
- this.EVENTS.MEASUREMENT_UPDATED,
+ this._broadcastChange(this.EVENTS.MEASUREMENT_UPDATED, {
source,
- newMeasurement
- );
+ measurement: newMeasurement,
+ });
} else {
log.info(`Measurement added.`, newMeasurement);
this.measurements[internalId] = newMeasurement;
- this._broadcastChange(
- this.EVENTS.MEASUREMENT_ADDED,
+ this._broadcastChange(this.EVENTS.MEASUREMENT_ADDED, {
source,
- newMeasurement
- );
+ measurement: newMeasurement,
+ });
}
return newMeasurement.id;
@@ -466,37 +467,45 @@ class MeasurementService {
newMeasurement
);
this.measurements[internalId] = newMeasurement;
- this._broadcastChange(
- this.EVENTS.MEASUREMENT_UPDATED,
+ this._broadcastChange(this.EVENTS.MEASUREMENT_UPDATED, {
source,
- newMeasurement
- );
+ measurement: newMeasurement,
+ notYetUpdatedAtSource: false,
+ });
} else {
log.info(`Measurement added.`, newMeasurement);
this.measurements[internalId] = newMeasurement;
- this._broadcastChange(
- this.EVENTS.MEASUREMENT_ADDED,
+ this._broadcastChange(this.EVENTS.MEASUREMENT_ADDED, {
source,
- newMeasurement
- );
+ measurement: newMeasurement,
+ });
}
return newMeasurement.id;
}
- remove(source, id) {
+ /**
+ * Removes a measurement and broadcasts the removed event.
+ *
+ * @param {string} id The measurement id
+ * @param {MeasurementSource} source The measurement source instance
+ * @return {string} The removed measurement id
+ */
+ remove(id, source) {
if (!id || !this.measurements[id]) {
log.warn(`No id provided, or unable to find measurement by id.`);
return;
}
delete this.measurements[id];
- this._broadcastChange(this.EVENTS.MEASUREMENT_REMOVED, source, id);
+ this._broadcastChange(this.EVENTS.MEASUREMENT_REMOVED, {
+ source,
+ measurement: id, // This is weird :shrug:
+ });
}
clearMeasurements() {
this.measurements = {};
-
this._broadcastChange(this.EVENTS.MEASUREMENTS_CLEARED);
}
@@ -556,7 +565,7 @@ class MeasurementService {
*
* @param {MeasurementSource} source Measurement source instance
* @param {string} definition The source definition
- * @param {string} measurement The measurement serice measurement
+ * @param {Measurement} measurement The measurement service measurement
* @return {Object} The mapping based on matched criteria
*/
_getMatchingMapping(source, definition, measurement) {
@@ -610,30 +619,20 @@ class MeasurementService {
/**
* Broadcasts measurement changes.
*
- * @param {string} eventName The event name
- * @param {MeasurementSource} source The measurement source
- * @param {string} measurement The measurement id
+ * @param {string} eventName The event name.add
+ * @param {object} eventData.source The measurement source.
+ * @param {object} eventData.measurement The measurement.
+ * @param {boolean} eventData.notYetUpdatedAtSource True if the measurement was edited
+ * within the measurement service and the source needs to update.
* @return void
*/
- _broadcastChange(eventName, source, measurement) {
+ _broadcastChange(eventName, eventData) {
const hasListeners = Object.keys(this.listeners).length > 0;
const hasCallbacks = Array.isArray(this.listeners[eventName]);
- if (!source) {
- /* Broadcast to all sources */
- /* Object.keys(this.sources).forEach(source => {
- if (hasListeners && hasCallbacks) {
- this.listeners[eventName].forEach(listener => {
- listener.callback({ source, measurement });
- });
- }
- });
- return; */
- }
-
if (hasListeners && hasCallbacks) {
this.listeners[eventName].forEach(listener => {
- listener.callback({ source, measurement });
+ listener.callback(eventData);
});
}
}
diff --git a/platform/core/src/services/UIDialogService/index.js b/platform/core/src/services/UIDialogService/index.js
index b703be0bd..c6e126ee5 100644
--- a/platform/core/src/services/UIDialogService/index.js
+++ b/platform/core/src/services/UIDialogService/index.js
@@ -57,6 +57,7 @@ function _create({
preservePosition = true,
isDraggable = true,
showOverlay = false,
+ onClickOutside,
defaultPosition,
}) {
return serviceImplementation._create({
@@ -69,6 +70,7 @@ function _create({
centralize,
preservePosition,
isDraggable,
+ onClickOutside,
showOverlay,
defaultPosition,
});
diff --git a/platform/ui/index.js b/platform/ui/index.js
index 113057bff..81379ac85 100644
--- a/platform/ui/index.js
+++ b/platform/ui/index.js
@@ -32,6 +32,7 @@ export {
export {
Button,
ButtonGroup,
+ ContextMenu,
DateRange,
Dialog,
Dropdown,
@@ -76,6 +77,7 @@ export {
ThumbnailTracked,
ThumbnailList,
ToolbarButton,
+ ContextMenuMeasurements,
Tooltip,
TooltipClipboard,
Typography,
diff --git a/platform/ui/src/components/ContextMenu/ContextMenu.jsx b/platform/ui/src/components/ContextMenu/ContextMenu.jsx
new file mode 100644
index 000000000..c02154959
--- /dev/null
+++ b/platform/ui/src/components/ContextMenu/ContextMenu.jsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Typography } from '@ohif/ui';
+
+const ContextMenu = ({ items }) => {
+ return (
+
e.preventDefault()}
+ >
+ {items.map((item, index) => (
+
item.action(item)}
+ className="flex px-4 py-3 cursor-pointer items-center transition duration-300 hover:bg-primary-dark border-b border-primary-dark last:border-b-0"
+ >
+ {item.label}
+
+ ))}
+
+ );
+};
+
+ContextMenu.propTypes = {
+ items: PropTypes.arrayOf(
+ PropTypes.shape({
+ label: PropTypes.string.isRequired,
+ actionType: PropTypes.string.isRequired,
+ action: PropTypes.func.isRequired,
+ })
+ ).isRequired,
+};
+
+export default ContextMenu;
diff --git a/platform/ui/src/components/ContextMenu/ContextMenu.mdx b/platform/ui/src/components/ContextMenu/ContextMenu.mdx
new file mode 100644
index 000000000..71486c6ec
--- /dev/null
+++ b/platform/ui/src/components/ContextMenu/ContextMenu.mdx
@@ -0,0 +1,49 @@
+---
+name: Context Menu
+menu: General
+route: components/contextMenu
+---
+
+import { Playground, Props } from 'docz';
+import { ContextMenu } from '@ohif/ui';
+
+# Context Menu
+
+...
+
+## Import
+
+```javascript
+import { ContextMenu } from '@ohif/ui';
+```
+
+
+ {() => {
+ const items = [
+ {
+ label: 'Delete measurement',
+ actionType: 'Delete',
+ action: () => alert('Delete'),
+ },
+ {
+ label: 'Relabel',
+ actionType: 'setLabel',
+ action: () => alert('Relabel'),
+ },
+ {
+ label: 'Add Description',
+ actionType: 'setDescription',
+ action: () => alert('Add Description'),
+ },
+ ];
+ return (
+
+
+
+ );
+ }}
+
+
+## Properties
+
+
diff --git a/platform/ui/src/components/ContextMenu/index.js b/platform/ui/src/components/ContextMenu/index.js
new file mode 100644
index 000000000..1bf5f07d6
--- /dev/null
+++ b/platform/ui/src/components/ContextMenu/index.js
@@ -0,0 +1 @@
+export { default } from './ContextMenu';
diff --git a/platform/ui/src/components/ContextMenuMeasurements/ContextMenuMeasurements.jsx b/platform/ui/src/components/ContextMenuMeasurements/ContextMenuMeasurements.jsx
new file mode 100644
index 000000000..04f732a6c
--- /dev/null
+++ b/platform/ui/src/components/ContextMenuMeasurements/ContextMenuMeasurements.jsx
@@ -0,0 +1,44 @@
+import { ContextMenu } from '@ohif/ui';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+const ContextMenuMeasurements = ({
+ onGetMenuItems,
+ onSetLabel,
+ onClose,
+ onDelete,
+}) => {
+ const defaultMenuItems = [
+ {
+ label: 'Delete measurement',
+ actionType: 'Delete',
+ action: item => {
+ onDelete(item);
+ onClose();
+ },
+ value: {},
+ },
+ {
+ label: 'Add Label',
+ actionType: 'setLabel',
+ action: item => {
+ onSetLabel(item);
+ onClose();
+ },
+ value: {},
+ },
+ ];
+
+ const menuItems = onGetMenuItems(defaultMenuItems);
+
+ return
;
+};
+
+ContextMenuMeasurements.propTypes = {
+ onClose: PropTypes.func.isRequired,
+ onSetLabel: PropTypes.func.isRequired,
+ onDelete: PropTypes.func.isRequired,
+ onGetMenuItems: PropTypes.func.isRequired,
+};
+
+export default ContextMenuMeasurements;
diff --git a/platform/ui/src/components/ContextMenuMeasurements/index.js b/platform/ui/src/components/ContextMenuMeasurements/index.js
new file mode 100644
index 000000000..fd58615db
--- /dev/null
+++ b/platform/ui/src/components/ContextMenuMeasurements/index.js
@@ -0,0 +1 @@
+export { default } from './ContextMenuMeasurements';
diff --git a/platform/ui/src/components/index.js b/platform/ui/src/components/index.js
index 1288505c8..ee1946abd 100644
--- a/platform/ui/src/components/index.js
+++ b/platform/ui/src/components/index.js
@@ -1,5 +1,6 @@
import Button from './Button';
import ButtonGroup from './ButtonGroup';
+import ContextMenu from './ContextMenu';
import DateRange from './DateRange';
import Dialog from './Dialog';
import Dropdown from './Dropdown';
@@ -41,6 +42,7 @@ import ThumbnailNoImage from './ThumbnailNoImage';
import ThumbnailTracked from './ThumbnailTracked';
import ThumbnailList from './ThumbnailList';
import ToolbarButton from './ToolbarButton';
+import ContextMenuMeasurements from './ContextMenuMeasurements';
import ExpandableToolbarButton from './ExpandableToolbarButton';
import ListMenu from './ListMenu';
import Tooltip from './Tooltip';
@@ -55,6 +57,7 @@ import ViewportPane from './ViewportPane';
export {
Button,
ButtonGroup,
+ ContextMenu,
DateRange,
Dialog,
Dropdown,
@@ -99,6 +102,7 @@ export {
ThumbnailTracked,
ThumbnailList,
ToolbarButton,
+ ContextMenuMeasurements,
Tooltip,
TooltipClipboard,
Typography,
diff --git a/platform/ui/src/contextProviders/DialogProvider.jsx b/platform/ui/src/contextProviders/DialogProvider.jsx
index ec57f0232..c7275038d 100644
--- a/platform/ui/src/contextProviders/DialogProvider.jsx
+++ b/platform/ui/src/contextProviders/DialogProvider.jsx
@@ -4,7 +4,7 @@ import React, {
useContext,
useCallback,
useEffect,
- useRef
+ useRef,
} from 'react';
import PropTypes from 'prop-types';
@@ -154,6 +154,7 @@ const DialogProvider = ({ children, service }) => {
onStart,
onStop,
onDrag,
+ onClickOutside,
showOverlay,
} = dialog;
@@ -169,7 +170,7 @@ const DialogProvider = ({ children, service }) => {
disabled={!isDraggable}
position={position}
defaultPosition={position}
- bounds='parent'
+ bounds="parent"
onStart={event => {
const e = event || window.event;
const target = e.target || e.srcElement;
@@ -225,16 +226,27 @@ const DialogProvider = ({ children, service }) => {
const background = 'bg-black bg-opacity-50';
const overlay = 'fixed z-50 left-0 top-0 w-full h-full overflow-auto';
return (
-
+
{component}
);
};
- return showOverlay ? withOverlay(dragableItem()) : dragableItem();
+ let result = dragableItem();
+
+ if (showOverlay) {
+ result = withOverlay(result);
+ }
+
+ if (typeof onClickOutside === 'function') {
+ result = (
+
+ {result}
+
+ );
+ }
+
+ return result;
});
/**
@@ -253,7 +265,7 @@ const DialogProvider = ({ children, service }) => {
};
const onKeyDownHandler = event => {
- if (event.key === "Escape") {
+ if (event.key === 'Escape') {
dismissAll();
}
};
@@ -262,11 +274,11 @@ const DialogProvider = ({ children, service }) => {
return (
- {!isEmpty() &&
-
+ {!isEmpty() && (
+
{renderDialogs()}
- }
+ )}
{children}
);
@@ -302,3 +314,29 @@ DialogProvider.propTypes = {
};
export default DialogProvider;
+
+function OutsideAlerter(props) {
+ const wrapperRef = useRef(null);
+
+ useEffect(() => {
+ /**
+ * Alert if clicked on outside of element
+ */
+ function handleInteractionOutside(event) {
+ if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
+ props.onClickOutside();
+ }
+ }
+
+ // Bind the event listener
+ document.addEventListener('mousedown', handleInteractionOutside);
+ document.addEventListener('touchstart', handleInteractionOutside);
+ return () => {
+ // Unbind the event listener on clean up
+ document.removeEventListener('mousedown', handleInteractionOutside);
+ document.removeEventListener('touchstart', handleInteractionOutside);
+ };
+ }, [wrapperRef]);
+
+ return
{props.children}
;
+}