[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:
parent
39e68f3193
commit
401a2e38c8
@ -30,8 +30,8 @@ export default {
|
|||||||
* @param {object} [configuration={}]
|
* @param {object} [configuration={}]
|
||||||
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
||||||
*/
|
*/
|
||||||
preRegistration({ servicesManager, configuration = {} }) {
|
preRegistration({ servicesManager, commandsManager, configuration = {} }) {
|
||||||
init({ servicesManager, configuration });
|
init({ servicesManager, commandsManager, configuration });
|
||||||
},
|
},
|
||||||
getViewportModule({ commandsManager }) {
|
getViewportModule({ commandsManager }) {
|
||||||
const ExtendedOHIFCornerstoneViewport = props => {
|
const ExtendedOHIFCornerstoneViewport = props => {
|
||||||
|
|||||||
@ -1,32 +1,239 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
import { Dialog, Input } from '@ohif/ui';
|
import { Input, Dialog, ContextMenuMeasurements } from '@ohif/ui';
|
||||||
import cornerstone from 'cornerstone-core';
|
import cs from 'cornerstone-core';
|
||||||
import csTools from 'cornerstone-tools';
|
import csTools from 'cornerstone-tools';
|
||||||
import merge from 'lodash.merge';
|
import merge from 'lodash.merge';
|
||||||
import initCornerstoneTools from './initCornerstoneTools.js';
|
import initCornerstoneTools from './initCornerstoneTools.js';
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
import './initWADOImageLoader.js';
|
||||||
import initWADOImageLoader from './initWADOImageLoader.js';
|
import getCornerstoneMeasurementById from './utils/getCornerstoneMeasurementById';
|
||||||
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
|
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
|
||||||
//
|
|
||||||
import { setEnabledElement } from './state';
|
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} servicesManager
|
||||||
* @param {Object} configuration
|
* @param {Object} configuration
|
||||||
* @param {Object|Array} configuration.csToolsConfig
|
* @param {Object|Array} configuration.csToolsConfig
|
||||||
*/
|
*/
|
||||||
export default function init({ servicesManager, configuration }) {
|
export default function init({
|
||||||
|
servicesManager,
|
||||||
|
commandsManager,
|
||||||
|
configuration,
|
||||||
|
}) {
|
||||||
const {
|
const {
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
} = servicesManager.services;
|
} = 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) {
|
if (UIDialogService) {
|
||||||
let dialogId = UIDialogService.create({
|
UIDialogService.create({
|
||||||
|
id: dialogId,
|
||||||
centralize: true,
|
centralize: true,
|
||||||
isDraggable: false,
|
isDraggable: false,
|
||||||
content: Dialog,
|
content: Dialog,
|
||||||
@ -34,34 +241,26 @@ export default function init({ servicesManager, configuration }) {
|
|||||||
showOverlay: true,
|
showOverlay: true,
|
||||||
contentProps: {
|
contentProps: {
|
||||||
title: 'Enter your annotation',
|
title: 'Enter your annotation',
|
||||||
value: { label: data ? data.text : '' },
|
value: { label },
|
||||||
noCloseButton: true,
|
noCloseButton: true,
|
||||||
onClose: () => UIDialogService.dismiss({ id: dialogId }),
|
onClose: () => UIDialogService.dismiss({ id: dialogId }),
|
||||||
actions: [
|
actions: [
|
||||||
{ id: 'cancel', text: 'Cancel', type: 'secondary' },
|
{ id: 'cancel', text: 'Cancel', type: 'secondary' },
|
||||||
{ id: 'save', text: 'Save', type: 'primary' },
|
{ id: 'save', text: 'Save', type: 'primary' },
|
||||||
],
|
],
|
||||||
onSubmit: ({ action, value }) => {
|
onSubmit: onSubmitHandler,
|
||||||
switch (action.id) {
|
|
||||||
case 'save':
|
|
||||||
callback(value.label);
|
|
||||||
break;
|
|
||||||
case 'cancel':
|
|
||||||
callback();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
UIDialogService.dismiss({ id: dialogId });
|
|
||||||
},
|
|
||||||
body: ({ value, setValue }) => {
|
body: ({ value, setValue }) => {
|
||||||
const onChangeHandler = event => {
|
const onChangeHandler = event => {
|
||||||
event.persist();
|
event.persist();
|
||||||
setValue(value => ({ ...value, label: event.target.value }));
|
setValue(value => ({ ...value, label: event.target.value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const onKeyPressHandler = event => {
|
const onKeyPressHandler = event => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
onSubmitHandler({ value, action: { id: 'save' } });
|
onSubmitHandler({ value, action: { id: 'save' } });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 bg-primary-dark">
|
<div className="p-4 bg-primary-dark">
|
||||||
<Input
|
<Input
|
||||||
@ -84,10 +283,7 @@ export default function init({ servicesManager, configuration }) {
|
|||||||
const { csToolsConfig } = configuration;
|
const { csToolsConfig } = configuration;
|
||||||
const metadataProvider = OHIF.cornerstone.metadataProvider;
|
const metadataProvider = OHIF.cornerstone.metadataProvider;
|
||||||
|
|
||||||
cornerstone.metaData.addProvider(
|
cs.metaData.addProvider(metadataProvider.get.bind(metadataProvider), 9999);
|
||||||
metadataProvider.get.bind(metadataProvider),
|
|
||||||
9999
|
|
||||||
);
|
|
||||||
|
|
||||||
// ~~
|
// ~~
|
||||||
const defaultCsToolsConfig = csToolsConfig || {
|
const defaultCsToolsConfig = csToolsConfig || {
|
||||||
@ -143,17 +339,14 @@ export default function init({ servicesManager, configuration }) {
|
|||||||
tools.push(...toolsGroupedByType[toolsGroup])
|
tools.push(...toolsGroupedByType[toolsGroup])
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Measurement Service */
|
|
||||||
_connectToolsToMeasurementService(MeasurementService, DisplaySetService);
|
|
||||||
|
|
||||||
/* Add extension tools configuration here. */
|
/* Add extension tools configuration here. */
|
||||||
const internalToolsConfig = {
|
const internalToolsConfig = {
|
||||||
ArrowAnnotate: {
|
ArrowAnnotate: {
|
||||||
configuration: {
|
configuration: {
|
||||||
getTextCallback: (callback, eventDetails) =>
|
getTextCallback: (callback, eventDetails) =>
|
||||||
callInputDialog(null, eventDetails, callback),
|
callInputDialog(null, callback),
|
||||||
changeTextCallback: (data, eventDetails, callback) =>
|
changeTextCallback: (data, eventDetails, callback) =>
|
||||||
callInputDialog(data, eventDetails, callback),
|
callInputDialog(data, callback),
|
||||||
allowEmptyLabel: true,
|
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('PanMultiTouch', { pointers: 2 }); // TODO: Better error if no options
|
||||||
csTools.setToolActive('ZoomTouchPinch', {});
|
csTools.setToolActive('ZoomTouchPinch', {});
|
||||||
csTools.setToolEnabled('Overlay', {});
|
csTools.setToolEnabled('Overlay', {});
|
||||||
|
|
||||||
|
cs.events.addEventListener(cs.EVENTS.ELEMENT_ENABLED, elementEnabledHandler);
|
||||||
|
cs.events.addEventListener(
|
||||||
|
cs.EVENTS.ELEMENT_DISABLED,
|
||||||
|
elementDisabledHandler
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const _initMeasurementService = (MeasurementService, DisplaySetService) => {
|
const _initMeasurementService = (MeasurementService, DisplaySetService) => {
|
||||||
@ -293,12 +492,11 @@ const _connectToolsToMeasurementService = (
|
|||||||
csToolsVer4MeasurementSource
|
csToolsVer4MeasurementSource
|
||||||
);
|
);
|
||||||
const { addOrUpdate, remove } = csToolsVer4MeasurementSource;
|
const { addOrUpdate, remove } = csToolsVer4MeasurementSource;
|
||||||
const elementEnabledEvt = cornerstone.EVENTS.ELEMENT_ENABLED;
|
const elementEnabledEvt = cs.EVENTS.ELEMENT_ENABLED;
|
||||||
|
|
||||||
/* Measurement Service Events */
|
/* Measurement Service Events */
|
||||||
cornerstone.events.addEventListener(elementEnabledEvt, evt => {
|
cs.events.addEventListener(elementEnabledEvt, evt => {
|
||||||
// TODO: Debounced update of measurements that are modified
|
// TODO: Debounced update of measurements that are modified
|
||||||
|
|
||||||
function addMeasurement(csToolsEvent) {
|
function addMeasurement(csToolsEvent) {
|
||||||
console.log('CSTOOLS::addOrUpdate', csToolsEvent, csToolsEvent.detail);
|
console.log('CSTOOLS::addOrUpdate', csToolsEvent, csToolsEvent.detail);
|
||||||
|
|
||||||
@ -322,13 +520,13 @@ const _connectToolsToMeasurementService = (
|
|||||||
if (!csToolsEvent.detail.measurementData.id) {
|
if (!csToolsEvent.detail.measurementData.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const evtDetail = csToolsEvent.detail;
|
const evtDetail = csToolsEvent.detail;
|
||||||
const { toolName, toolType, measurementData } = evtDetail;
|
const { toolName, toolType, measurementData } = evtDetail;
|
||||||
const csToolName = toolName || measurementData.toolType || toolType;
|
const csToolName = toolName || measurementData.toolType || toolType;
|
||||||
|
|
||||||
evtDetail.id = csToolsEvent.detail.measurementData.id;
|
evtDetail.id = csToolsEvent.detail.measurementData.id;
|
||||||
addOrUpdate(csToolName, evtDetail);
|
addOrUpdate(csToolName, evtDetail);
|
||||||
//
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to update measurement:', 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, () => {
|
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 enabledElement = evt.detail.element;
|
||||||
const completedEvt = csTools.EVENTS.MEASUREMENT_COMPLETED;
|
const completedEvt = csTools.EVENTS.MEASUREMENT_COMPLETED;
|
||||||
const updatedEvt = csTools.EVENTS.MEASUREMENT_MODIFIED;
|
const updatedEvt = csTools.EVENTS.MEASUREMENT_MODIFIED;
|
||||||
@ -368,25 +594,17 @@ const _connectToolsToMeasurementService = (
|
|||||||
enabledElement.addEventListener(updatedEvt, updateMeasurement);
|
enabledElement.addEventListener(updatedEvt, updateMeasurement);
|
||||||
enabledElement.addEventListener(removedEvt, removeMeasurement);
|
enabledElement.addEventListener(removedEvt, removeMeasurement);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return csToolsVer4MeasurementSource;
|
||||||
};
|
};
|
||||||
|
|
||||||
const _connectMeasurementServiceToTools = (
|
const _connectMeasurementServiceToTools = (
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
measurementSource
|
measurementSource
|
||||||
) => {
|
) => {
|
||||||
const {
|
const { MEASUREMENT_REMOVED } = MeasurementService.EVENTS;
|
||||||
MEASUREMENTS_CLEARED,
|
|
||||||
MEASUREMENT_REMOVED,
|
|
||||||
} = MeasurementService.EVENTS;
|
|
||||||
const sourceId = measurementSource.id;
|
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
|
// TODO: This is an unsafe delete
|
||||||
// Cornerstone-tools should probably expose a more generic "delete by id"
|
// 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
|
// And have toolState managers expose a method to find any of their toolState by ID
|
||||||
@ -401,7 +619,7 @@ const _connectMeasurementServiceToTools = (
|
|||||||
MEASUREMENT_REMOVED,
|
MEASUREMENT_REMOVED,
|
||||||
({ source, measurement: removedMeasurementId }) => {
|
({ source, measurement: removedMeasurementId }) => {
|
||||||
// THIS POINTS TO ORIGINAL; Not a copy
|
// THIS POINTS TO ORIGINAL; Not a copy
|
||||||
const imageIdSpecificToolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState();
|
const imageIdSpecificToolState = globalImageIdSpecificToolStateManager.saveToolState();
|
||||||
|
|
||||||
// ImageId -->
|
// ImageId -->
|
||||||
Object.keys(imageIdSpecificToolState).forEach(imageId => {
|
Object.keys(imageIdSpecificToolState).forEach(imageId => {
|
||||||
@ -425,37 +643,7 @@ const _connectMeasurementServiceToTools = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// const {
|
const _getDefaultPosition = event => ({
|
||||||
// MEASUREMENT_ADDED,
|
x: (event && event.currentPoints.client.x) || 0,
|
||||||
// MEASUREMENT_UPDATED,
|
y: (event && event.currentPoints.client.y) || 0,
|
||||||
// } = 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);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|||||||
@ -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 SUPPORTED_TOOLS from './constants/supportedTools';
|
||||||
import getPointsFromHandles from './utils/getPointsFromHandles';
|
import getPointsFromHandles from './utils/getPointsFromHandles';
|
||||||
|
import getHandlesFromPoints from './utils/getHandlesFromPoints';
|
||||||
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||||
|
|
||||||
const ArrowAnnotate = {
|
const ArrowAnnotate = {
|
||||||
toAnnotation: (measurement, definition) => {
|
toAnnotation: (measurement, definition) => {},
|
||||||
// TODO -> Implement when this is needed.
|
|
||||||
},
|
|
||||||
toMeasurement: (
|
toMeasurement: (
|
||||||
csToolsAnnotation,
|
csToolsAnnotation,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
@ -40,7 +39,7 @@ const ArrowAnnotate = {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: measurementData.id,
|
id: measurementData.id,
|
||||||
SOPInstanceUID: SOPInstanceUID,
|
SOPInstanceUID,
|
||||||
FrameOfReferenceUID,
|
FrameOfReferenceUID,
|
||||||
referenceSeriesUID: SeriesInstanceUID,
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
referenceStudyUID: StudyInstanceUID,
|
referenceStudyUID: StudyInstanceUID,
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import SUPPORTED_TOOLS from './constants/supportedTools';
|
import SUPPORTED_TOOLS from './constants/supportedTools';
|
||||||
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||||
|
import getHandlesFromPoints from './utils/getHandlesFromPoints';
|
||||||
|
|
||||||
const Bidirectional = {
|
const Bidirectional = {
|
||||||
toAnnotation: (measurement, definition) => {
|
toAnnotation: (measurement, definition) => {},
|
||||||
// TODO -> Implement when this is needed.
|
|
||||||
},
|
|
||||||
toMeasurement: (
|
toMeasurement: (
|
||||||
csToolsAnnotation,
|
csToolsAnnotation,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
@ -41,12 +40,12 @@ const Bidirectional = {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: measurementData.id,
|
id: measurementData.id,
|
||||||
SOPInstanceUID: SOPInstanceUID,
|
SOPInstanceUID,
|
||||||
FrameOfReferenceUID,
|
FrameOfReferenceUID,
|
||||||
referenceSeriesUID: SeriesInstanceUID,
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
referenceStudyUID: StudyInstanceUID,
|
referenceStudyUID: StudyInstanceUID,
|
||||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||||
label: measurementData.text,
|
label: measurementData.label,
|
||||||
description: measurementData.description,
|
description: measurementData.description,
|
||||||
unit: measurementData.unit,
|
unit: measurementData.unit,
|
||||||
shortestDiameter: measurementData.shortestDiameter,
|
shortestDiameter: measurementData.shortestDiameter,
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import SUPPORTED_TOOLS from './constants/supportedTools';
|
import SUPPORTED_TOOLS from './constants/supportedTools';
|
||||||
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||||
|
import getHandlesFromPoints from './utils/getHandlesFromPoints';
|
||||||
|
|
||||||
const EllipticalRoi = {
|
const EllipticalRoi = {
|
||||||
toAnnotation: (measurement, definition) => {
|
toAnnotation: (measurement, definition) => {},
|
||||||
// TODO -> Implement when this is needed.
|
|
||||||
},
|
|
||||||
toMeasurement: (
|
toMeasurement: (
|
||||||
csToolsAnnotation,
|
csToolsAnnotation,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
@ -63,12 +62,12 @@ const EllipticalRoi = {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: measurementData.id,
|
id: measurementData.id,
|
||||||
SOPInstanceUID: SOPInstanceUID,
|
SOPInstanceUID,
|
||||||
FrameOfReferenceUID,
|
FrameOfReferenceUID,
|
||||||
referenceSeriesUID: SeriesInstanceUID,
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
referenceStudyUID: StudyInstanceUID,
|
referenceStudyUID: StudyInstanceUID,
|
||||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||||
label: measurementData.text,
|
label: measurementData.label,
|
||||||
description: measurementData.description,
|
description: measurementData.description,
|
||||||
unit: measurementData.unit,
|
unit: measurementData.unit,
|
||||||
area:
|
area:
|
||||||
|
|||||||
@ -4,32 +4,7 @@ import getPointsFromHandles from './utils/getPointsFromHandles';
|
|||||||
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||||
|
|
||||||
const Length = {
|
const Length = {
|
||||||
toAnnotation: (measurement, definition) => {
|
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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps cornerstone annotation event data to measurement service format.
|
* Maps cornerstone annotation event data to measurement service format.
|
||||||
@ -68,12 +43,12 @@ const Length = {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: measurementData.id,
|
id: measurementData.id,
|
||||||
SOPInstanceUID: SOPInstanceUID,
|
SOPInstanceUID,
|
||||||
FrameOfReferenceUID,
|
FrameOfReferenceUID,
|
||||||
referenceSeriesUID: SeriesInstanceUID,
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
referenceStudyUID: StudyInstanceUID,
|
referenceStudyUID: StudyInstanceUID,
|
||||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||||
label: measurementData.text,
|
label: measurementData.label,
|
||||||
description: measurementData.description,
|
description: measurementData.description,
|
||||||
unit: measurementData.unit,
|
unit: measurementData.unit,
|
||||||
length: measurementData.length,
|
length: measurementData.length,
|
||||||
|
|||||||
@ -44,8 +44,8 @@ describe('measurementServiceMappings.js', () => {
|
|||||||
toolName: definition,
|
toolName: definition,
|
||||||
measurementData: {
|
measurementData: {
|
||||||
_measurementServiceId: 1,
|
_measurementServiceId: 1,
|
||||||
sopInstanceUid: '123',
|
SOPInstanceUID: '123',
|
||||||
frameOfReferenceUID: '123',
|
FrameOfReferenceUID: '123',
|
||||||
SeriesInstanceUID: '123',
|
SeriesInstanceUID: '123',
|
||||||
handles,
|
handles,
|
||||||
text: 'Test',
|
text: 'Test',
|
||||||
|
|||||||
@ -1,4 +1,13 @@
|
|||||||
export default function getHandlesFromPoints(points) {
|
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
|
return points
|
||||||
.map((p, i) => (i % 10 === 0 ? { start: p } : { end: p }))
|
.map((p, i) => (i % 10 === 0 ? { start: p } : { end: p }))
|
||||||
.reduce((obj, item) => Object.assign(obj, { ...item }), {});
|
.reduce((obj, item) => Object.assign(obj, { ...item }), {});
|
||||||
|
|||||||
@ -162,19 +162,24 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
|||||||
const onMeasurementItemEditHandler = ({ id }) => {
|
const onMeasurementItemEditHandler = ({ id }) => {
|
||||||
const measurement = MeasurementService.getMeasurement(id);
|
const measurement = MeasurementService.getMeasurement(id);
|
||||||
|
|
||||||
let dialogId;
|
|
||||||
const onSubmitHandler = ({ action, value }) => {
|
const onSubmitHandler = ({ action, value }) => {
|
||||||
switch (action.id) {
|
switch (action.id) {
|
||||||
case 'save': {
|
case 'save': {
|
||||||
MeasurementService.update(id, {
|
MeasurementService.update(
|
||||||
|
id,
|
||||||
|
{
|
||||||
...measurement,
|
...measurement,
|
||||||
...value,
|
...value,
|
||||||
});
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UIDialogService.dismiss({ id: dialogId });
|
UIDialogService.dismiss({ id: 'enter-annotation' });
|
||||||
};
|
};
|
||||||
dialogId = UIDialogService.create({
|
|
||||||
|
UIDialogService.create({
|
||||||
|
id: 'enter-annotation',
|
||||||
centralize: true,
|
centralize: true,
|
||||||
isDraggable: false,
|
isDraggable: false,
|
||||||
useLastPosition: false,
|
useLastPosition: false,
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
|
|||||||
*
|
*
|
||||||
* @typedef {Object} Measurement
|
* @typedef {Object} Measurement
|
||||||
* @property {number} id -
|
* @property {number} id -
|
||||||
* @property {string} sopInstanceUid -
|
* @property {string} SOPInstanceUID -
|
||||||
* @property {string} FrameOfReferenceUID -
|
* @property {string} FrameOfReferenceUID -
|
||||||
* @property {string} referenceSeriesUID -
|
* @property {string} referenceSeriesUID -
|
||||||
* @property {string} label -
|
* @property {string} label -
|
||||||
@ -50,6 +50,7 @@ const MEASUREMENT_SCHEMA_KEYS = [
|
|||||||
|
|
||||||
const EVENTS = {
|
const EVENTS = {
|
||||||
MEASUREMENT_UPDATED: 'event::measurement_updated',
|
MEASUREMENT_UPDATED: 'event::measurement_updated',
|
||||||
|
INTERNAL_MEASUREMENT_UPDATED: 'event:internal_measurement_updated',
|
||||||
MEASUREMENT_ADDED: 'event::measurement_added',
|
MEASUREMENT_ADDED: 'event::measurement_added',
|
||||||
MEASUREMENT_REMOVED: 'event::measurement_removed',
|
MEASUREMENT_REMOVED: 'event::measurement_removed',
|
||||||
MEASUREMENTS_CLEARED: 'event::measurements_cleared',
|
MEASUREMENTS_CLEARED: 'event::measurements_cleared',
|
||||||
@ -146,7 +147,7 @@ class MeasurementService {
|
|||||||
return this.addOrUpdate(source, definition, measurement);
|
return this.addOrUpdate(source, definition, measurement);
|
||||||
};
|
};
|
||||||
source.remove = id => {
|
source.remove = id => {
|
||||||
return this.remove(source, id);
|
return this.remove(id, source);
|
||||||
};
|
};
|
||||||
source.getAnnotation = (definition, measurementId) => {
|
source.getAnnotation = (definition, measurementId) => {
|
||||||
return this.getAnnotation(source, definition, measurementId);
|
return this.getAnnotation(source, definition, measurementId);
|
||||||
@ -216,11 +217,6 @@ class MeasurementService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!toSourceSchema) {
|
|
||||||
log.warn('Source mapping function not provided. Exiting early.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!toMeasurementSchema) {
|
if (!toMeasurementSchema) {
|
||||||
log.warn('Measurement mapping function not provided. Exiting early.');
|
log.warn('Measurement mapping function not provided. Exiting early.');
|
||||||
return;
|
return;
|
||||||
@ -269,9 +265,9 @@ class MeasurementService {
|
|||||||
measurementId,
|
measurementId,
|
||||||
definition
|
definition
|
||||||
);
|
);
|
||||||
|
const measurement = this.getMeasurement(measurementId);
|
||||||
if (mapping) return mapping.toSourceSchema(measurement, definition);
|
if (mapping) return mapping.toSourceSchema(measurement, definition);
|
||||||
|
|
||||||
const measurement = this.getMeasurement(measurementId);
|
|
||||||
const matchingMapping = this._getMatchingMapping(
|
const matchingMapping = this._getMatchingMapping(
|
||||||
source,
|
source,
|
||||||
definition,
|
definition,
|
||||||
@ -285,21 +281,28 @@ class MeasurementService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update(id, measurement) {
|
update(id, measurement, notYetUpdatedAtSource = false) {
|
||||||
if (this.measurements[id]) {
|
if (this.measurements[id]) {
|
||||||
const updatedMeasurement = {
|
const updatedMeasurement = {
|
||||||
...measurement,
|
...measurement,
|
||||||
modifiedTimestamp: Math.floor(Date.now() / 1000),
|
modifiedTimestamp: Math.floor(Date.now() / 1000),
|
||||||
};
|
};
|
||||||
|
|
||||||
log.info(`Updating measurement...`, updatedMeasurement);
|
log.info(
|
||||||
|
`Updating internal measurement representation...`,
|
||||||
|
updatedMeasurement
|
||||||
|
);
|
||||||
|
|
||||||
this.measurements[id] = updatedMeasurement;
|
this.measurements[id] = updatedMeasurement;
|
||||||
|
|
||||||
this._broadcastChange(
|
this._broadcastChange(
|
||||||
|
// Add an internal flag to say the measurement has not yet been updated at source.
|
||||||
this.EVENTS.MEASUREMENT_UPDATED,
|
this.EVENTS.MEASUREMENT_UPDATED,
|
||||||
measurement.source,
|
{
|
||||||
updatedMeasurement
|
source: measurement.source,
|
||||||
|
measurement: updatedMeasurement,
|
||||||
|
notYetUpdatedAtSource,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return updatedMeasurement.id;
|
return updatedMeasurement.id;
|
||||||
@ -375,19 +378,17 @@ class MeasurementService {
|
|||||||
newMeasurement
|
newMeasurement
|
||||||
);
|
);
|
||||||
this.measurements[internalId] = newMeasurement;
|
this.measurements[internalId] = newMeasurement;
|
||||||
this._broadcastChange(
|
this._broadcastChange(this.EVENTS.MEASUREMENT_UPDATED, {
|
||||||
this.EVENTS.MEASUREMENT_UPDATED,
|
|
||||||
source,
|
source,
|
||||||
newMeasurement
|
measurement: newMeasurement,
|
||||||
);
|
});
|
||||||
} else {
|
} else {
|
||||||
log.info(`Measurement added.`, newMeasurement);
|
log.info(`Measurement added.`, newMeasurement);
|
||||||
this.measurements[internalId] = newMeasurement;
|
this.measurements[internalId] = newMeasurement;
|
||||||
this._broadcastChange(
|
this._broadcastChange(this.EVENTS.MEASUREMENT_ADDED, {
|
||||||
this.EVENTS.MEASUREMENT_ADDED,
|
|
||||||
source,
|
source,
|
||||||
newMeasurement
|
measurement: newMeasurement,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return newMeasurement.id;
|
return newMeasurement.id;
|
||||||
@ -466,37 +467,45 @@ class MeasurementService {
|
|||||||
newMeasurement
|
newMeasurement
|
||||||
);
|
);
|
||||||
this.measurements[internalId] = newMeasurement;
|
this.measurements[internalId] = newMeasurement;
|
||||||
this._broadcastChange(
|
this._broadcastChange(this.EVENTS.MEASUREMENT_UPDATED, {
|
||||||
this.EVENTS.MEASUREMENT_UPDATED,
|
|
||||||
source,
|
source,
|
||||||
newMeasurement
|
measurement: newMeasurement,
|
||||||
);
|
notYetUpdatedAtSource: false,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
log.info(`Measurement added.`, newMeasurement);
|
log.info(`Measurement added.`, newMeasurement);
|
||||||
this.measurements[internalId] = newMeasurement;
|
this.measurements[internalId] = newMeasurement;
|
||||||
this._broadcastChange(
|
this._broadcastChange(this.EVENTS.MEASUREMENT_ADDED, {
|
||||||
this.EVENTS.MEASUREMENT_ADDED,
|
|
||||||
source,
|
source,
|
||||||
newMeasurement
|
measurement: newMeasurement,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return newMeasurement.id;
|
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]) {
|
if (!id || !this.measurements[id]) {
|
||||||
log.warn(`No id provided, or unable to find measurement by id.`);
|
log.warn(`No id provided, or unable to find measurement by id.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
delete this.measurements[id];
|
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() {
|
clearMeasurements() {
|
||||||
this.measurements = {};
|
this.measurements = {};
|
||||||
|
|
||||||
this._broadcastChange(this.EVENTS.MEASUREMENTS_CLEARED);
|
this._broadcastChange(this.EVENTS.MEASUREMENTS_CLEARED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -556,7 +565,7 @@ class MeasurementService {
|
|||||||
*
|
*
|
||||||
* @param {MeasurementSource} source Measurement source instance
|
* @param {MeasurementSource} source Measurement source instance
|
||||||
* @param {string} definition The source definition
|
* @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
|
* @return {Object} The mapping based on matched criteria
|
||||||
*/
|
*/
|
||||||
_getMatchingMapping(source, definition, measurement) {
|
_getMatchingMapping(source, definition, measurement) {
|
||||||
@ -610,30 +619,20 @@ class MeasurementService {
|
|||||||
/**
|
/**
|
||||||
* Broadcasts measurement changes.
|
* Broadcasts measurement changes.
|
||||||
*
|
*
|
||||||
* @param {string} eventName The event name
|
* @param {string} eventName The event name.add
|
||||||
* @param {MeasurementSource} source The measurement source
|
* @param {object} eventData.source The measurement source.
|
||||||
* @param {string} measurement The measurement id
|
* @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
|
* @return void
|
||||||
*/
|
*/
|
||||||
_broadcastChange(eventName, source, measurement) {
|
_broadcastChange(eventName, eventData) {
|
||||||
const hasListeners = Object.keys(this.listeners).length > 0;
|
const hasListeners = Object.keys(this.listeners).length > 0;
|
||||||
const hasCallbacks = Array.isArray(this.listeners[eventName]);
|
const hasCallbacks = Array.isArray(this.listeners[eventName]);
|
||||||
|
|
||||||
if (!source) {
|
|
||||||
/* Broadcast to all sources */
|
|
||||||
/* Object.keys(this.sources).forEach(source => {
|
|
||||||
if (hasListeners && hasCallbacks) {
|
if (hasListeners && hasCallbacks) {
|
||||||
this.listeners[eventName].forEach(listener => {
|
this.listeners[eventName].forEach(listener => {
|
||||||
listener.callback({ source, measurement });
|
listener.callback(eventData);
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return; */
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasListeners && hasCallbacks) {
|
|
||||||
this.listeners[eventName].forEach(listener => {
|
|
||||||
listener.callback({ source, measurement });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,6 +57,7 @@ function _create({
|
|||||||
preservePosition = true,
|
preservePosition = true,
|
||||||
isDraggable = true,
|
isDraggable = true,
|
||||||
showOverlay = false,
|
showOverlay = false,
|
||||||
|
onClickOutside,
|
||||||
defaultPosition,
|
defaultPosition,
|
||||||
}) {
|
}) {
|
||||||
return serviceImplementation._create({
|
return serviceImplementation._create({
|
||||||
@ -69,6 +70,7 @@ function _create({
|
|||||||
centralize,
|
centralize,
|
||||||
preservePosition,
|
preservePosition,
|
||||||
isDraggable,
|
isDraggable,
|
||||||
|
onClickOutside,
|
||||||
showOverlay,
|
showOverlay,
|
||||||
defaultPosition,
|
defaultPosition,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -32,6 +32,7 @@ export {
|
|||||||
export {
|
export {
|
||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
|
ContextMenu,
|
||||||
DateRange,
|
DateRange,
|
||||||
Dialog,
|
Dialog,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
@ -76,6 +77,7 @@ export {
|
|||||||
ThumbnailTracked,
|
ThumbnailTracked,
|
||||||
ThumbnailList,
|
ThumbnailList,
|
||||||
ToolbarButton,
|
ToolbarButton,
|
||||||
|
ContextMenuMeasurements,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipClipboard,
|
TooltipClipboard,
|
||||||
Typography,
|
Typography,
|
||||||
|
|||||||
34
platform/ui/src/components/ContextMenu/ContextMenu.jsx
Normal file
34
platform/ui/src/components/ContextMenu/ContextMenu.jsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Typography } from '@ohif/ui';
|
||||||
|
|
||||||
|
const ContextMenu = ({ items }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="relative bg-secondary-dark rounded z-50 block w-48"
|
||||||
|
onContextMenu={e => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
onClick={() => 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"
|
||||||
|
>
|
||||||
|
<Typography>{item.label}</Typography>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ContextMenu.propTypes = {
|
||||||
|
items: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
actionType: PropTypes.string.isRequired,
|
||||||
|
action: PropTypes.func.isRequired,
|
||||||
|
})
|
||||||
|
).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContextMenu;
|
||||||
49
platform/ui/src/components/ContextMenu/ContextMenu.mdx
Normal file
49
platform/ui/src/components/ContextMenu/ContextMenu.mdx
Normal file
@ -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';
|
||||||
|
```
|
||||||
|
|
||||||
|
<Playground>
|
||||||
|
{() => {
|
||||||
|
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 (
|
||||||
|
<div className="p-4">
|
||||||
|
<ContextMenu items={items} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Playground>
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
<Props of={ContextMenu} />
|
||||||
1
platform/ui/src/components/ContextMenu/index.js
Normal file
1
platform/ui/src/components/ContextMenu/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './ContextMenu';
|
||||||
@ -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 <ContextMenu items={menuItems} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
ContextMenuMeasurements.propTypes = {
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
onSetLabel: PropTypes.func.isRequired,
|
||||||
|
onDelete: PropTypes.func.isRequired,
|
||||||
|
onGetMenuItems: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContextMenuMeasurements;
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export { default } from './ContextMenuMeasurements';
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import ButtonGroup from './ButtonGroup';
|
import ButtonGroup from './ButtonGroup';
|
||||||
|
import ContextMenu from './ContextMenu';
|
||||||
import DateRange from './DateRange';
|
import DateRange from './DateRange';
|
||||||
import Dialog from './Dialog';
|
import Dialog from './Dialog';
|
||||||
import Dropdown from './Dropdown';
|
import Dropdown from './Dropdown';
|
||||||
@ -41,6 +42,7 @@ import ThumbnailNoImage from './ThumbnailNoImage';
|
|||||||
import ThumbnailTracked from './ThumbnailTracked';
|
import ThumbnailTracked from './ThumbnailTracked';
|
||||||
import ThumbnailList from './ThumbnailList';
|
import ThumbnailList from './ThumbnailList';
|
||||||
import ToolbarButton from './ToolbarButton';
|
import ToolbarButton from './ToolbarButton';
|
||||||
|
import ContextMenuMeasurements from './ContextMenuMeasurements';
|
||||||
import ExpandableToolbarButton from './ExpandableToolbarButton';
|
import ExpandableToolbarButton from './ExpandableToolbarButton';
|
||||||
import ListMenu from './ListMenu';
|
import ListMenu from './ListMenu';
|
||||||
import Tooltip from './Tooltip';
|
import Tooltip from './Tooltip';
|
||||||
@ -55,6 +57,7 @@ import ViewportPane from './ViewportPane';
|
|||||||
export {
|
export {
|
||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
|
ContextMenu,
|
||||||
DateRange,
|
DateRange,
|
||||||
Dialog,
|
Dialog,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
@ -99,6 +102,7 @@ export {
|
|||||||
ThumbnailTracked,
|
ThumbnailTracked,
|
||||||
ThumbnailList,
|
ThumbnailList,
|
||||||
ToolbarButton,
|
ToolbarButton,
|
||||||
|
ContextMenuMeasurements,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipClipboard,
|
TooltipClipboard,
|
||||||
Typography,
|
Typography,
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import React, {
|
|||||||
useContext,
|
useContext,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@ -154,6 +154,7 @@ const DialogProvider = ({ children, service }) => {
|
|||||||
onStart,
|
onStart,
|
||||||
onStop,
|
onStop,
|
||||||
onDrag,
|
onDrag,
|
||||||
|
onClickOutside,
|
||||||
showOverlay,
|
showOverlay,
|
||||||
} = dialog;
|
} = dialog;
|
||||||
|
|
||||||
@ -169,7 +170,7 @@ const DialogProvider = ({ children, service }) => {
|
|||||||
disabled={!isDraggable}
|
disabled={!isDraggable}
|
||||||
position={position}
|
position={position}
|
||||||
defaultPosition={position}
|
defaultPosition={position}
|
||||||
bounds='parent'
|
bounds="parent"
|
||||||
onStart={event => {
|
onStart={event => {
|
||||||
const e = event || window.event;
|
const e = event || window.event;
|
||||||
const target = e.target || e.srcElement;
|
const target = e.target || e.srcElement;
|
||||||
@ -225,16 +226,27 @@ const DialogProvider = ({ children, service }) => {
|
|||||||
const background = 'bg-black bg-opacity-50';
|
const background = 'bg-black bg-opacity-50';
|
||||||
const overlay = 'fixed z-50 left-0 top-0 w-full h-full overflow-auto';
|
const overlay = 'fixed z-50 left-0 top-0 w-full h-full overflow-auto';
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={classNames(overlay, background)} key={id}>
|
||||||
className={classNames(overlay, background)}
|
|
||||||
key={id}
|
|
||||||
>
|
|
||||||
{component}
|
{component}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return showOverlay ? withOverlay(dragableItem()) : dragableItem();
|
let result = dragableItem();
|
||||||
|
|
||||||
|
if (showOverlay) {
|
||||||
|
result = withOverlay(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof onClickOutside === 'function') {
|
||||||
|
result = (
|
||||||
|
<OutsideAlerter onClickOutside={onClickOutside}>
|
||||||
|
{result}
|
||||||
|
</OutsideAlerter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -253,7 +265,7 @@ const DialogProvider = ({ children, service }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onKeyDownHandler = event => {
|
const onKeyDownHandler = event => {
|
||||||
if (event.key === "Escape") {
|
if (event.key === 'Escape') {
|
||||||
dismissAll();
|
dismissAll();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -262,11 +274,11 @@ const DialogProvider = ({ children, service }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContext.Provider value={{ create, dismiss, dismissAll, isEmpty }}>
|
<DialogContext.Provider value={{ create, dismiss, dismissAll, isEmpty }}>
|
||||||
{!isEmpty() &&
|
{!isEmpty() && (
|
||||||
<div className='w-full h-full absolute' onKeyDown={onKeyDownHandler}>
|
<div className="w-full h-full absolute" onKeyDown={onKeyDownHandler}>
|
||||||
{renderDialogs()}
|
{renderDialogs()}
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
{children}
|
{children}
|
||||||
</DialogContext.Provider>
|
</DialogContext.Provider>
|
||||||
);
|
);
|
||||||
@ -302,3 +314,29 @@ DialogProvider.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default DialogProvider;
|
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 <div ref={wrapperRef}>{props.children}</div>;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user