feat: Add White Labelling, Hanging Protocols, and tons of Extensions / Mode fixes

* modified

* new files in existing extensions/libraries/modes

* fixed bugs and tests

* Clean up

* fix: viewport display fixed

* Fixed configs for deployment

* Removed unnecessary files and functions

* Fixed default HP module

* Added white labelling

* Fixed hash routing

* Removed unnecessary routers

Co-authored-by: Alireza Sedghi <ar.sedghi@gmail.com>
This commit is contained in:
Erik ZieglerandAlireza Sedghi authored and GitHub committed 2021-05-28 14:32:48 -04:00
1 parent 0db81b30f3
commit 99b8dc4759
93 files changed
+3825 -1293

No files matched your search

+2 -2
View File
@@ -32,7 +32,7 @@
"@ohif/core": "^0.50.0",
"@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.3.0",
"cornerstone-math": "^0.1.8",
"cornerstone-math": "^0.1.9",
"cornerstone-tools": "5.1.2",
"cornerstone-wado-image-loader": "^3.1.2",
"dcmjs": "0.16.1",
@@ -47,6 +47,6 @@
"@babel/runtime": "7.7.6",
"lodash.merge": "^4.6.2",
"lodash.debounce": "4.0.8",
"react-cornerstone-viewport": "4.0.2"
"react-cornerstone-viewport": "4.0.5"
}
}
@@ -1,107 +1,75 @@
import React, { Component } from 'react';
import React, { useEffect, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import CornerstoneViewport from 'react-cornerstone-viewport';
import OHIF from '@ohif/core';
import csTools from 'cornerstone-tools';
import PropTypes from 'prop-types';
import debounce from 'lodash.debounce';
import getTools from './utils/getTools.js';
import setActiveAndPassiveToolsForElement from './utils/setActiveAndPassiveToolsForElement';
import ViewportLoadingIndicator from './ViewportLoadingIndicator';
import setCornerstoneMeasurementActive from './_shared/setCornerstoneMeasurementActive';
import ViewportOverlay from './ViewportOverlay';
import { setEnabledElement } from './state';
import { useCine, useViewportGrid } from '@ohif/ui';
const scrollToIndex = cornerstoneTools.importInternal('util/scrollToIndex');
const { StackManager } = OHIF.utils;
class OHIFCornerstoneViewport extends Component {
state = {
viewportData: null,
};
function OHIFCornerstoneViewport({
children,
dataSource,
displaySet,
onElementEnabled,
element,
viewportIndex,
servicesManager,
commandsManager,
}) {
const {
ToolBarService,
DisplaySetService,
MeasurementService,
HangingProtocolService,
} = servicesManager.services;
const [viewportData, setViewportData] = useState(null);
const [{ cines }, cineService] = useCine();
const [{ viewports }, viewportGridService] = useViewportGrid();
static defaultProps = {
customProps: {},
};
const isMounted = useRef(false);
const stageChangedRef = useRef(false);
static propTypes = {
displaySet: PropTypes.object,
viewportIndex: PropTypes.number,
dataSource: PropTypes.object,
children: PropTypes.node,
customProps: PropTypes.object,
ToolBarService: PropTypes.object,
};
static name = 'OHIFCornerstoneViewport';
static init() {
console.log('OHIFCornerstoneViewport init()');
}
static destroy() {
console.log('OHIFCornerstoneViewport destroy()');
StackManager.clearStacks();
}
/**
* Obtain the CornerstoneTools Stack for the specified display set.
*
* @param {Object} displaySet
* @param {Object} dataSource
* @return {Object} CornerstoneTools Stack
*/
static getCornerstoneStack(displaySet, dataSource) {
const { imageIndex } = displaySet;
// Get stack from Stack Manager
const storedStack = StackManager.findOrCreateStack(displaySet, dataSource);
// Clone the stack here so we don't mutate it
const stack = Object.assign({}, storedStack);
stack.currentImageIdIndex = imageIndex;
// TODO -> Do we ever use this like this?
// if (SOPInstanceUID) {
// const index = stack.imageIds.findIndex(imageId => {
// const imageIdSOPInstanceUID = cornerstone.metaData.get(
// 'SOPInstanceUID',
// imageId
// );
// return imageIdSOPInstanceUID === SOPInstanceUID;
// });
// if (index > -1) {
// stack.currentImageIdIndex = index;
// } else {
// console.warn(
// 'SOPInstanceUID provided was not found in specified DisplaySet'
// );
// }
// }
return stack;
}
getViewportData = async displaySet => {
let viewportData;
const { dataSource } = this.props;
const stack = OHIFCornerstoneViewport.getCornerstoneStack(
displaySet,
dataSource
);
viewportData = {
StudyInstanceUID: displaySet.StudyInstanceUID,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
stack,
const onNewImage = (element, callback) => {
const handler = () => {
element.removeEventListener(cornerstone.EVENTS.IMAGE_RENDERED, handler);
callback(element, ToolBarService);
};
return viewportData;
element.addEventListener(cornerstone.EVENTS.IMAGE_RENDERED, handler);
};
setStateFromProps() {
const { displaySet } = this.props;
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
StackManager.clearStacks();
};
}, []);
useEffect(() => {
const { unsubscribe } = HangingProtocolService.subscribe(
HangingProtocolService.EVENTS.STAGE_CHANGE,
() => {
stageChangedRef.current = true;
}
);
return () => {
unsubscribe();
};
}, []);
useEffect(() => {
cineService.setCine({ id: viewportIndex });
}, [viewportIndex]);
useEffect(() => {
const {
StudyInstanceUID,
displaySetInstanceUID,
@@ -118,111 +86,267 @@ class OHIFCornerstoneViewport extends Component {
);
}
this.getViewportData(displaySet).then(viewportData => {
this.setState({
viewportData,
_getViewportData(dataSource, displaySet).then(data => {
if (isMounted.current) setViewportData(data);
});
}, [dataSource, displaySet, viewportIndex]);
useEffect(() => {
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
MeasurementService,
DisplaySetService,
element,
viewportIndex,
displaySet.displaySetInstanceUID,
viewportGridService
);
_checkForCachedJumpToMeasurementEvents(
MeasurementService,
DisplaySetService,
element,
viewportIndex,
displaySet.displaySetInstanceUID,
viewportGridService
);
// reseting the HP stage changed flag
if (element) {
onNewImage(element, () => {
stageChangedRef.current = false;
});
}
// running HP-defined callbacks: invert, window level ...
if (element && displaySet.renderedCallback) {
onNewImage(element, displaySet.renderedCallback);
}
return () => {
unsubscribeFromJumpToMeasurementEvents();
};
}, [element, displaySet]);
let childrenWithProps = null;
if (!viewportData) {
return null;
}
const {
imageIds,
initialImageIdIndex,
// If this comes from the instance, would be a better default
// `FrameTime` in the instance
// frameRate = 0,
} = viewportData.stack;
// TODO: Does it make more sense to use Context?
if (children && children.length) {
childrenWithProps = children.map((child, index) => {
return (
child &&
React.cloneElement(child, {
viewportIndex,
key: index,
})
);
});
}
componentDidMount() {
this.setStateFromProps();
}
// We have...
// StudyInstanceUid, DisplaySetInstanceUid
// Use displaySetInstanceUid --> SeriesInstanceUid
// Get meta for series, map to actionBar
// const displaySet = DisplaySetService.getDisplaySetByUID(
// dSet.displaySetInstanceUID
// );
// TODO: This display contains the meta for all instances.
// That can't be right...
// console.log('DISPLAYSET', displaySet);
// const seriesMeta = DicomMetadataStore.getSeries(this.props.displaySet.StudyInstanceUID, '');
// console.log(seriesMeta);
componentDidUpdate(prevProps) {
const { displaySet } = this.props;
const prevDisplaySet = prevProps.displaySet;
const cine = cines[viewportIndex];
const isPlaying = (cine && cine.isPlaying) || false;
const frameRate = (cine && cine.frameRate) || 24;
if (
displaySet.displaySetInstanceUID !==
prevDisplaySet.displaySetInstanceUID ||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
displaySet.imageIndex !== prevDisplaySet.imageIndex
) {
this.setStateFromProps();
}
}
return (
<div className="relative flex flex-row w-full h-full overflow-hidden">
<CornerstoneViewport
onElementEnabled={onElementEnabled}
viewportIndex={viewportIndex}
imageIds={imageIds}
imageIdIndex={initialImageIdIndex}
initialViewport={displaySet.initialViewport} // from hanging protocol
stageChanged={stageChangedRef.current}
// Sync resize throttle w/ sidepanel animation duration to prevent
// seizure inducing strobe blinking effect
resizeRefreshRateMs={150}
// TODO: ViewportGrid Context?
isActive={true} // todo
isStackPrefetchEnabled={true} // todo
isPlaying={isPlaying}
frameRate={frameRate}
isOverlayVisible={true}
loadingIndicatorComponent={ViewportLoadingIndicator}
viewportOverlayComponent={props => {
return (
<ViewportOverlay
{...props}
activeTools={ToolBarService.getActiveTools()}
/>
);
}}
/>
{childrenWithProps}
</div>
);
}
render() {
let childrenWithProps = null;
OHIFCornerstoneViewport.propTypes = {
displaySet: PropTypes.object,
viewportIndex: PropTypes.number,
dataSource: PropTypes.object,
children: PropTypes.node,
customProps: PropTypes.object,
ToolBarService: PropTypes.object,
};
if (!this.state.viewportData) {
return null;
}
const { viewportIndex } = this.props;
const {
imageIds,
currentImageIdIndex,
// If this comes from the instance, would be a better default
// `FrameTime` in the instance
// frameRate = 0,
} = this.state.viewportData.stack;
OHIFCornerstoneViewport.defaultProps = {
customProps: {},
};
// TODO: Does it make more sense to use Context?
if (this.props.children && this.props.children.length) {
childrenWithProps = this.props.children.map((child, index) => {
return (
child &&
React.cloneElement(child, {
viewportIndex: this.props.viewportIndex,
key: index,
})
const _viewportLabels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
function _getCornerstoneStack(displaySet, dataSource) {
// Get stack from Stack Manager
const storedStack = StackManager.findOrCreateStack(displaySet, dataSource);
// Clone the stack here so we don't mutate it
const stack = Object.assign({}, storedStack);
return stack;
}
async function _getViewportData(dataSource, displaySet) {
const stack = _getCornerstoneStack(displaySet, dataSource);
const viewportData = {
StudyInstanceUID: displaySet.StudyInstanceUID,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
stack,
};
return viewportData;
}
function _subscribeToJumpToMeasurementEvents(
MeasurementService,
DisplaySetService,
element,
viewportIndex,
displaySetInstanceUID,
viewportGridService
) {
const { unsubscribe } = MeasurementService.subscribe(
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT,
({ measurement }) => {
if (!measurement) return;
// check if the correct viewport index.
// if (viewportIndex !== jumpToMeasurementViewportIndex) {
// // Event for a different viewport.
// return;
// }
// Jump the the measurement if the displaySetInstanceUID matches
if (measurement.displaySetInstanceUID === displaySetInstanceUID) {
_jumpToMeasurement(
measurement,
element,
viewportIndex,
MeasurementService,
DisplaySetService,
viewportGridService
);
});
}
}
);
return unsubscribe;
}
function _checkForCachedJumpToMeasurementEvents(
MeasurementService,
DisplaySetService,
element,
viewportIndex,
displaySetInstanceUID,
viewportGridService
) {
// Check if there is a queued jumpToMeasurement event
const measurementIdToJumpTo = MeasurementService.getJumpToMeasurement(
viewportIndex
);
if (measurementIdToJumpTo && element) {
// Jump to measurement if the measurement exists
const measurement = MeasurementService.getMeasurement(
measurementIdToJumpTo
);
if (measurement.displaySetInstanceUID === displaySetInstanceUID) {
_jumpToMeasurement(
measurement,
element,
viewportIndex,
MeasurementService,
DisplaySetService,
viewportGridService
);
}
}
}
function _jumpToMeasurement(
measurement,
targetElement,
viewportIndex,
MeasurementService,
DisplaySetService,
viewportGridService
) {
const { displaySetInstanceUID, SOPInstanceUID } = measurement;
const referencedDisplaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
const imageIndex = referencedDisplaySet.images.findIndex(
i => i.SOPInstanceUID === SOPInstanceUID
);
setCornerstoneMeasurementActive(measurement);
viewportGridService.setActiveViewportIndex(viewportIndex);
if (targetElement !== null) {
const enabledElement = cornerstone.getEnabledElement(targetElement);
// Wait for the image to update or we get a race condition when the element has only just been enabled.
const scrollToHandler = evt => {
scrollToIndex(targetElement, imageIndex);
targetElement.removeEventListener(
'cornerstoneimagerendered',
scrollToHandler
);
};
targetElement.addEventListener('cornerstoneimagerendered', scrollToHandler);
if (enabledElement.image) {
cornerstone.updateImage(targetElement);
}
const debouncedNewImageHandler = debounce(
({ currentImageIdIndex, sopInstanceUid }) => {
const { displaySet } = this.props;
const { StudyInstanceUID } = displaySet;
if (currentImageIdIndex > 0) {
this.props.onNewImage({
StudyInstanceUID,
SOPInstanceUID: sopInstanceUid,
imageIndex: currentImageIdIndex,
activeViewportIndex: viewportIndex,
});
}
},
700
);
// TODO -> We may still want a wrapped component to define all the measurement api stuff.
return (
<>
<CornerstoneViewport
viewportIndex={viewportIndex}
imageIds={imageIds}
imageIdIndex={currentImageIdIndex}
onNewImage={debouncedNewImageHandler}
// TODO: ViewportGrid Context?
isActive={true} // todo
isStackPrefetchEnabled={true} // todo
isPlaying={false}
frameRate={24}
// TODO: How do we share/track this?
// For example, Tracked Extension also wraps this component
// Could use window? Doesn't have to be reactive
// Need to expose viewportGrid as a "UI Service"
onElementEnabled={evt => {
const enabledElement = evt.detail.element;
const tools = getTools();
const toolAlias = ToolBarService.state.primaryToolId;
setEnabledElement(viewportIndex, enabledElement);
setActiveAndPassiveToolsForElement(enabledElement, tools);
csTools.setToolActiveForElement(enabledElement, toolAlias, {
mouseButtonMask: 1,
});
}}
// Sync resize throttle w/ sidepanel animation duration to prevent
// seizure inducing strobe blinking effect
resizeRefreshRateMs={150}
/>
{childrenWithProps}
</>
);
// Jump to measurement consumed, remove.
MeasurementService.removeJumpToMeasurement(viewportIndex);
}
}
@@ -0,0 +1,41 @@
import React from 'react';
import PropTypes from 'prop-types';
const ViewportLoadingIndicator = ({ error }) => {
if (error) {
return (
<>
<div className="bg-black h-full w-full absolute opacity-50"></div>
<div className="text-primary-light text-xl font-thin">
<h4>Error Loading Image</h4>
<p>An error has occurred.</p>
<p>{error.message}</p>
</div>
</>
);
}
return (
<>
<div className="bg-black h-full w-full absolute opacity-50"></div>
<div className="absolute transparent w-full h-full flex items-center justify-center">
<p className="text-primary-light text-xl font-thin">
Loading...
</p>
</div>
</>
);
};
ViewportLoadingIndicator.propTypes = {
percentComplete: PropTypes.number,
error: PropTypes.object,
};
ViewportLoadingIndicator.defaultProps = {
percentComplete: 0,
error: null,
};
export default ViewportLoadingIndicator;
@@ -0,0 +1,83 @@
import React from 'react';
import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core';
import classnames from 'classnames';
const ViewportOverlay = ({
imageId,
scale,
windowWidth,
windowCenter,
imageIndex,
stackSize,
activeTools,
}) => {
const topLeft = 'top-viewport left-viewport';
const topRight = 'top-viewport right-viewport-scrollbar';
const bottomRight = 'bottom-viewport right-viewport-scrollbar';
const bottomLeft = 'bottom-viewport left-viewport';
const overlay = 'absolute pointer-events-none';
const isZoomActive = activeTools.includes('Zoom');
const isWwwcActive = activeTools.includes('Wwwc');
if (!imageId) {
return null;
}
// TODO: this component should be presentational only. Right now it has a weird dependency on Cornerstone
const generalImageModule =
cornerstone.metaData.get('generalImageModule', imageId) || {};
const { instanceNumber } = generalImageModule;
return (
<div className="text-primary-light">
<div data-cy={"viewport-overlay-top-left"} className={classnames(overlay, topLeft)}>
{isZoomActive && (
<div className="flex flex-row">
<span className="mr-1">Zoom:</span>
<span className="font-thin">{scale.toFixed(2)}x</span>
</div>
)}
{isWwwcActive && (
<div className="flex flex-row">
<span className="mr-1">W:</span>
<span className="ml-1 mr-2 font-thin">
{windowWidth.toFixed(0)}
</span>
<span className="mr-1">L:</span>
<span className="ml-1 font-thin">{windowCenter.toFixed(0)}</span>
</div>
)}
</div>
<div data-cy={"viewport-overlay-top-right"} className={classnames(overlay, topRight)}>
{stackSize > 1 && (
<div className="flex flex-row">
<span className="mr-1">I:</span>
<span className="font-thin">
{`${instanceNumber} (${imageIndex}/${stackSize})`}
</span>
</div>
)}
</div>
<div data-cy={"viewport-overlay-bottom-right"} className={classnames(overlay, bottomRight)}></div>
<div data-cy={"viewport-overlay-bottom-left"} className={classnames(overlay, bottomLeft)}></div>
</div>
);
};
ViewportOverlay.propTypes = {
scale: PropTypes.number.isRequired,
windowWidth: PropTypes.number.isRequired,
windowCenter: PropTypes.number.isRequired,
imageId: PropTypes.string.isRequired,
imageIndex: PropTypes.number.isRequired,
stackSize: PropTypes.number.isRequired,
activeTools: PropTypes.arrayOf(PropTypes.string),
};
ViewportOverlay.defaultProps = {
activeTools: [],
};
export default ViewportOverlay;
@@ -0,0 +1,36 @@
import csTools from 'cornerstone-tools';
const toolsGroupedByType = {
touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool],
annotations: [
csTools.ArrowAnnotateTool,
csTools.BidirectionalTool,
csTools.LengthTool,
csTools.AngleTool,
csTools.FreehandRoiTool,
csTools.EllipticalRoiTool,
csTools.DragProbeTool,
csTools.RectangleRoiTool,
],
other: [
csTools.PanTool,
csTools.ZoomTool,
csTools.WwwcTool,
csTools.WwwcRegionTool,
csTools.MagnifyTool,
csTools.StackScrollTool,
csTools.StackScrollMouseWheelTool,
csTools.OverlayTool,
],
};
export default function getTools() {
const tools = [];
Object.keys(toolsGroupedByType).forEach(toolsGroup =>
tools.push(...toolsGroupedByType[toolsGroup])
);
return tools;
}
export { toolsGroupedByType };
@@ -0,0 +1,21 @@
import csTools from 'cornerstone-tools';
export default function _setActiveAndPassiveToolsForElement(element, tools) {
const BaseAnnotationTool = csTools.importInternal('base/BaseAnnotationTool');
tools.forEach(tool => {
if (tool.prototype instanceof BaseAnnotationTool) {
// BaseAnnotationTool would likely come from csTools lib exports
const toolName = new tool().name;
csTools.setToolPassiveForElement(element, toolName); // there may be a better place to determine name; may not be on uninstantiated class
}
});
csTools.setToolActiveForElement(element, 'Pan', { mouseButtonMask: 4 });
csTools.setToolActiveForElement(element, 'Zoom', { mouseButtonMask: 2 });
csTools.setToolActiveForElement(element, 'Wwwc', { mouseButtonMask: 1 });
csTools.setToolActiveForElement(element, 'StackScrollMouseWheel', {}); // TODO: Empty options should not be required
csTools.setToolActiveForElement(element, 'PanMultiTouch', { pointers: 2 }); // TODO: Better error if no options
csTools.setToolActiveForElement(element, 'ZoomTouchPinch', {});
csTools.setToolEnabledForElement(element, 'Overlay', {});
}
@@ -0,0 +1,39 @@
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
const { globalImageIdSpecificToolStateManager } = cornerstoneTools;
export default function setCornerstoneMeasurementActive(measurement) {
const { id } = measurement;
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
Object.keys(toolState).forEach(imageId => {
const imageIdSpecificToolState = toolState[imageId];
Object.keys(imageIdSpecificToolState).forEach(toolType => {
const toolSpecificToolState = imageIdSpecificToolState[toolType];
const toolSpecificToolData = toolSpecificToolState.data;
if (toolSpecificToolData && toolSpecificToolData.length) {
toolSpecificToolData.forEach(data => {
data.active = data.id === id ? true : false;
});
}
});
});
const enabledElements = cornerstoneTools.store.state.enabledElements;
enabledElements.forEach(element => {
try {
cornerstone.updateImage(element);
} catch (ex) {
// https://github.com/cornerstonejs/cornerstone/blob/master/src/updateImage.js#L16
// This fails if enabledElement.image is undefined and we have no layers
// Instead of throwing, it should _probably_ do nothing.
// We'll just swallow the exception
}
});
}
+85 -4
View File
@@ -9,6 +9,30 @@ const scroll = cornerstoneTools.import('util/scroll');
const { studyMetadataManager } = OHIF.utils;
const imagePositionSynchronizer = new cornerstoneTools.Synchronizer(
cornerstone.EVENTS.NEW_IMAGE,
cornerstoneTools.stackImagePositionSynchronizer
);
const panZoomSynchronizer = new cornerstoneTools.Synchronizer(
cornerstone.EVENTS.IMAGE_RENDERED,
cornerstoneTools.panZoomSynchronizer
);
function onElementEnabledAddToSync(event) {
const { element } = event.detail;
imagePositionSynchronizer.add(element);
// panZoomSynchronizer.add(element);
}
function onElementDisabledRemoveFromSync(event) {
const { element } = event.detail;
imagePositionSynchronizer.remove(element);
panZoomSynchronizer.remove(element);
}
const commandsModule = ({ servicesManager, commandsManager }) => {
const { ViewportGridService } = servicesManager.services;
@@ -70,8 +94,60 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
cornerstone.reset(enabledElement);
}
},
invertViewport: () => {
const enabledElement = _getActiveViewportsEnabledElement();
toggleSynchronizer: ({ toggledState }) => {
const synchronizers = [imagePositionSynchronizer];
// Set synchronizer state when the command is run.
synchronizers.forEach(s => {
s.enabled = toggledState;
});
const unsubscribe = () => {
cornerstone.events.removeEventListener(
cornerstone.EVENTS.ELEMENT_ENABLED,
onElementEnabledAddToSync
);
cornerstone.events.removeEventListener(
cornerstone.EVENTS.ELEMENT_DISABLED,
onElementDisabledRemoveFromSync
);
};
const subscribe = () => {
cornerstone.events.addEventListener(
cornerstone.EVENTS.ELEMENT_ENABLED,
onElementEnabledAddToSync
);
cornerstone.events.addEventListener(
cornerstone.EVENTS.ELEMENT_DISABLED,
onElementDisabledRemoveFromSync
);
};
// Add event handlers so that if the layout is changed, new elements
// are automatically added to the synchronizer while it is enabled.
if (toggledState === true) {
subscribe();
} else {
// If the synchronizer is disabled, remove the event handlers
unsubscribe();
}
// Erase existing state and then set up all currently existing elements
cornerstone.getEnabledElements().map(e => {
synchronizers.forEach(s => {
s.remove(e.element);
s.add(e.element);
});
});
return unsubscribe;
},
invertViewport: ({ element }) => {
let enabledElement;
if (element === undefined) {
enabledElement = _getActiveViewportsEnabledElement();
} else {
enabledElement = element;
}
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
@@ -111,11 +187,11 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
const viewportInfo = getEnabledElement(i);
const hasCornerstoneContext =
viewportInfo.context == 'ACTIVE_VIEWPORT::CORNERSTONE';
viewportInfo.context === 'ACTIVE_VIEWPORT::CORNERSTONE';
if (hasCornerstoneContext) {
cornerstoneTools.setToolActiveForElement(
viewportInfo.enabledElement,
viewportInfo.element,
toolName,
{ mouseButtonMask: 1 }
);
@@ -333,6 +409,11 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
storeContexts: [],
options: {},
},
toggleSynchronizer: {
commandFn: actions.toggleSynchronizer,
storeContexts: [],
options: {},
},
removeToolState: {
commandFn: actions.removeToolState,
storeContexts: [],
+3
View File
@@ -0,0 +1,3 @@
const id = 'org.ohif.cornerstone';
export default id;
+3 -1
View File
@@ -38,12 +38,14 @@ export default {
const onNewImageHandler = jumpData => {
commandsManager.runCommand('jumpToImage', jumpData);
};
const { ToolBarService } = servicesManager;
const { ToolBarService } = servicesManager.services;
return (
<OHIFCornerstoneViewport
{...props}
ToolBarService={ToolBarService}
servicesManager={servicesManager}
commandsManager={commandsManager}
onNewImage={onNewImageHandler}
/>
);
+89 -2
View File
@@ -465,6 +465,9 @@ const _connectToolsToMeasurementService = (
}
);
// on display sets added, check if there are any measurements in measurement service that need to be
// put into cornerstone tools tooldata
const enabledElement = evt.detail.element;
const completedEvt = csTools.EVENTS.MEASUREMENT_COMPLETED;
const updatedEvt = csTools.EVENTS.MEASUREMENT_MODIFIED;
@@ -480,9 +483,13 @@ const _connectToolsToMeasurementService = (
const _connectMeasurementServiceToTools = (
MeasurementService,
measurementSource
measurementSource,
dataSource
) => {
const { MEASUREMENT_REMOVED } = MeasurementService.EVENTS;
const {
MEASUREMENT_REMOVED,
RAW_MEASUREMENT_ADDED,
} = MeasurementService.EVENTS;
const sourceId = measurementSource.id;
// TODO: This is an unsafe delete
@@ -495,6 +502,86 @@ const _connectMeasurementServiceToTools = (
//
// Could potentially use "source" from event to determine tool type and skip some
// iterations?
const {
POLYLINE,
ELLIPSE,
POINT,
BIDIRECTIONAL,
} = MeasurementService.VALUE_TYPES;
// TODO -> I get why this was attemped, but its not nearly flexible enough.
// A single measurement may have an ellipse + a bidirectional measurement, for instances.
// You can't define a bidirectional tool as a single type..
// OHIF-230
const TOOL_TYPE_TO_VALUE_TYPE = {
Length: POLYLINE,
EllipticalRoi: ELLIPSE,
Bidirectional: BIDIRECTIONAL,
ArrowAnnotate: POINT,
};
const VALUE_TYPE_TO_TOOL_TYPE = {
[POLYLINE]: 'Length',
[ELLIPSE]: 'EllipticalRoi',
[BIDIRECTIONAL]: 'Bidirectional',
[POINT]: 'ArrowAnnotate',
};
MeasurementService.subscribe(
RAW_MEASUREMENT_ADDED,
({ source, measurement, data, dataSource }) => {
const {
referenceStudyUID: StudyInstanceUID,
referenceSeriesUID: SeriesInstanceUID,
SOPInstanceUID,
} = measurement;
let toolType;
try {
toolType = VALUE_TYPE_TO_TOOL_TYPE[measurement.type];
} catch {
throw Error('Cannot add tool to cornerstone tools');
}
let imageId;
if (data.imageId) {
// handle dicom json launch, since we cannot create image id from
// instance UIDs, each tool should embed the instance metadata
// TODO: handle multi instance case
imageId = data.imageId;
} else {
// handle general case of dicom web
const instance = {
StudyInstanceUID,
SeriesInstanceUID,
SOPInstanceUID,
};
// TODO: handle multi frame
imageId = dataSource.getImageIdsForInstance({ instance });
}
const toolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState();
if (toolState[imageId] === undefined) {
toolState[imageId] = {};
}
const imageIdToolState = toolState[imageId];
// If we don't have tool state for this type of tool, add an empty object
if (imageIdToolState[toolType] === undefined) {
imageIdToolState[toolType] = {
data: [],
};
}
const toolData = imageIdToolState[toolType];
toolData.data.push(data);
}
);
MeasurementService.subscribe(
MEASUREMENT_REMOVED,
({ source, measurement: removedMeasurementId }) => {
+1 -1
View File
@@ -10,7 +10,7 @@ const state = {
* @return void
*/
const setEnabledElement = (viewportIndex, element, context) => {
const targetContext = context || DEFAULT_CONTEXT;
const targetContext = context || state.DEFAULT_CONTEXT;
state.enabledElements[viewportIndex] = {
element,