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:
1 parent
0db81b30f3
commit
99b8dc4759
93 files changed
+3825
-1293
No files matched your search
@@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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: [],
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
const id = 'org.ohif.cornerstone';
|
||||
|
||||
export default id;
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
import { DicomMetadataStore, IWebApiDataSource } from '@ohif/core';
|
||||
import OHIF from '@ohif/core';
|
||||
|
||||
import getImageId from '../DicomWebDataSource/utils/getImageId';
|
||||
|
||||
const metadataProvider = OHIF.classes.metadataProvider;
|
||||
|
||||
const mappings = {
|
||||
studyInstanceUid: 'StudyInstanceUID',
|
||||
patientId: 'PatientID',
|
||||
};
|
||||
|
||||
let _store = {
|
||||
urls: [],
|
||||
// {
|
||||
// url: url1
|
||||
// studies: [Study1, Study2], // if multiple studies
|
||||
// }
|
||||
// {
|
||||
// url: url2
|
||||
// studies: [Study1],
|
||||
// }
|
||||
// }
|
||||
};
|
||||
|
||||
const getMetaDataByURL = url => {
|
||||
return _store.urls.find(metaData => metaData.url === url);
|
||||
};
|
||||
|
||||
const findStudies = (key, value) => {
|
||||
let studies = [];
|
||||
_store.urls.map(metaData => {
|
||||
metaData.studies.map(aStudy => {
|
||||
if (aStudy[key] === value) {
|
||||
studies.push(aStudy);
|
||||
}
|
||||
});
|
||||
});
|
||||
return studies;
|
||||
};
|
||||
|
||||
function createDicomJSONApi(dicomJsonConfig) {
|
||||
const { name } = dicomJsonConfig;
|
||||
|
||||
const implementation = {
|
||||
parseRouteParams: async ({ params, query, url }) => {
|
||||
if (!url) url = query.get('url');
|
||||
let metaData = getMetaDataByURL(url);
|
||||
|
||||
// if we have already cached the data from this specific url
|
||||
// We are only handling one StudyInstanceUID to run; however,
|
||||
// all studies for patientID will be put in the correct tab
|
||||
if (metaData) {
|
||||
return metaData.studies.map(aStudy => {
|
||||
return aStudy.StudyInstanceUID;
|
||||
});
|
||||
}
|
||||
|
||||
const response = await fetch(url);
|
||||
let data = await response.json();
|
||||
|
||||
const studyInstanceUIDs = data.studies.map(
|
||||
study => study.StudyInstanceUID
|
||||
);
|
||||
|
||||
let StudyInstanceUID;
|
||||
let SeriesInstanceUID;
|
||||
data.studies.forEach(study => {
|
||||
StudyInstanceUID = study.StudyInstanceUID;
|
||||
|
||||
study.series.forEach(series => {
|
||||
SeriesInstanceUID = series.SeriesInstanceUID;
|
||||
|
||||
series.instances.forEach(instance => {
|
||||
const { url: imageId, metadata: naturalizedDicom } = instance;
|
||||
|
||||
// Add imageId specific mapping to this data as the URL isn't necessarliy WADO-URI.
|
||||
metadataProvider.addImageIdToUIDs(imageId, {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID: naturalizedDicom.SOPInstanceUID,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
_store.urls.push({
|
||||
url,
|
||||
studies: [...data.studies],
|
||||
});
|
||||
|
||||
return studyInstanceUIDs;
|
||||
},
|
||||
query: {
|
||||
studies: {
|
||||
mapParams: () => {},
|
||||
search: async param => {
|
||||
const [key, value] = Object.entries(param)[0];
|
||||
const mappedParam = mappings[key];
|
||||
|
||||
const studies = findStudies(mappedParam, value);
|
||||
|
||||
return studies.map(aStudy => {
|
||||
return {
|
||||
accession: aStudy.AccessionNumber,
|
||||
date: aStudy.StudyDate,
|
||||
description: aStudy.StudyDescription,
|
||||
instances: aStudy.NumInstances,
|
||||
modalities: aStudy.Modalities,
|
||||
mrn: aStudy.PatientID,
|
||||
patientName: aStudy.PatientName,
|
||||
studyInstanceUid: aStudy.StudyInstanceUID,
|
||||
NumInstances: aStudy.NumInstances,
|
||||
time: aStudy.StudyTime,
|
||||
};
|
||||
});
|
||||
},
|
||||
processResults: () => {
|
||||
console.debug(' DICOMJson QUERY processResults');
|
||||
},
|
||||
},
|
||||
series: {
|
||||
// mapParams: mapParams.bind(),
|
||||
search: () => {
|
||||
console.debug(' DICOMJson QUERY SERIES SEARCH');
|
||||
},
|
||||
},
|
||||
instances: {
|
||||
search: () => {
|
||||
console.debug(' DICOMJson QUERY instances SEARCH');
|
||||
},
|
||||
},
|
||||
},
|
||||
retrieve: {
|
||||
series: {
|
||||
metaData: () => {
|
||||
console.debug(' DICOMJson retrieve series metadata');
|
||||
},
|
||||
},
|
||||
},
|
||||
store: {
|
||||
dicom: () => {
|
||||
console.debug(' DICOMJson store dicom');
|
||||
},
|
||||
},
|
||||
retrieveSeriesMetadata: ({
|
||||
StudyInstanceUID,
|
||||
madeInClient = false,
|
||||
customSort,
|
||||
} = {}) => {
|
||||
if (!StudyInstanceUID) {
|
||||
throw new Error(
|
||||
'Unable to query for SeriesMetadata without StudyInstanceUID'
|
||||
);
|
||||
}
|
||||
|
||||
const study = findStudies('StudyInstanceUID', StudyInstanceUID)[0];
|
||||
let series;
|
||||
|
||||
if (customSort) {
|
||||
series = customSort(study.series);
|
||||
} else {
|
||||
series = study.series;
|
||||
}
|
||||
|
||||
const seriesSummaryMetadata = series.map(series => {
|
||||
const seriesSummary = {
|
||||
StudyInstanceUID: study.StudyInstanceUID,
|
||||
...series,
|
||||
};
|
||||
delete seriesSummary.instances;
|
||||
return seriesSummary;
|
||||
});
|
||||
|
||||
// Async load series, store as retrieved
|
||||
function storeInstances(naturalizedInstances) {
|
||||
DicomMetadataStore.addInstances(naturalizedInstances, madeInClient);
|
||||
}
|
||||
|
||||
DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata, madeInClient);
|
||||
|
||||
function setSuccessFlag() {
|
||||
const study = DicomMetadataStore.getStudy(
|
||||
StudyInstanceUID,
|
||||
madeInClient
|
||||
);
|
||||
study.isLoaded = true;
|
||||
}
|
||||
|
||||
const numberOfSeries = series.length;
|
||||
series.forEach((series, index) => {
|
||||
const instances = series.instances.map(instance => {
|
||||
const obj = {
|
||||
...instance.metadata,
|
||||
url: instance.url,
|
||||
imageId: instance.url,
|
||||
...series,
|
||||
};
|
||||
delete obj.instances;
|
||||
return obj;
|
||||
});
|
||||
storeInstances(instances);
|
||||
if (index === numberOfSeries - 1) setSuccessFlag();
|
||||
});
|
||||
},
|
||||
getImageIdsForDisplaySet(displaySet) {
|
||||
const images = displaySet.images;
|
||||
const imageIds = [];
|
||||
|
||||
if (!images) {
|
||||
return imageIds;
|
||||
}
|
||||
|
||||
displaySet.images.forEach(instance => {
|
||||
const NumberOfFrames = instance.NumberOfFrames;
|
||||
|
||||
if (NumberOfFrames > 1) {
|
||||
for (let i = 0; i < NumberOfFrames; i++) {
|
||||
const imageId = getImageId({
|
||||
instance,
|
||||
frame: i,
|
||||
config: dicomJsonConfig,
|
||||
});
|
||||
imageIds.push(imageId);
|
||||
}
|
||||
} else {
|
||||
const imageId = getImageId({ instance, config: dicomJsonConfig });
|
||||
imageIds.push(imageId);
|
||||
}
|
||||
});
|
||||
|
||||
return imageIds;
|
||||
},
|
||||
getImageIdsForInstance({ instance, frame }) {
|
||||
const imageIds = getImageId({
|
||||
instance,
|
||||
frame,
|
||||
});
|
||||
return imageIds;
|
||||
},
|
||||
};
|
||||
return IWebApiDataSource.create(implementation);
|
||||
}
|
||||
|
||||
export { createDicomJSONApi };
|
||||
@@ -63,10 +63,22 @@ function createDicomWebApi(dicomWebConfig) {
|
||||
const wadoDicomWebClient = new api.DICOMwebClient(wadoConfig);
|
||||
|
||||
const implementation = {
|
||||
parseRouteParams: ({ params, query }) => {
|
||||
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params;
|
||||
const queryStudyInstanceUIDs = query.get('StudyInstanceUIDs');
|
||||
|
||||
const StudyInstanceUIDs =
|
||||
queryStudyInstanceUIDs || paramsStudyInstanceUIDs;
|
||||
const StudyInstanceUIDsAsArray =
|
||||
StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs)
|
||||
? StudyInstanceUIDs
|
||||
: [StudyInstanceUIDs];
|
||||
return StudyInstanceUIDsAsArray;
|
||||
},
|
||||
query: {
|
||||
studies: {
|
||||
mapParams: mapParams.bind(),
|
||||
search: async function (origParams) {
|
||||
search: async function(origParams) {
|
||||
const { studyInstanceUid, seriesInstanceUid, ...mappedParams } =
|
||||
mapParams(origParams, {
|
||||
supportsFuzzyMatching,
|
||||
@@ -86,7 +98,7 @@ function createDicomWebApi(dicomWebConfig) {
|
||||
},
|
||||
series: {
|
||||
// mapParams: mapParams.bind(),
|
||||
search: async function (studyInstanceUid) {
|
||||
search: async function(studyInstanceUid) {
|
||||
const results = await seriesInStudy(
|
||||
qidoDicomWebClient,
|
||||
studyInstanceUid
|
||||
@@ -129,7 +141,6 @@ function createDicomWebApi(dicomWebConfig) {
|
||||
DicomMetadataStore.addInstances(naturalizedInstances);
|
||||
callback(naturalizedInstances);
|
||||
};
|
||||
|
||||
const studyPromises = StudyInstanceUIDs.map(StudyInstanceUID =>
|
||||
retrieveStudyMetadata(
|
||||
wadoDicomWebClient,
|
||||
@@ -177,7 +188,14 @@ function createDicomWebApi(dicomWebConfig) {
|
||||
await wadoDicomWebClient.storeInstances(options);
|
||||
},
|
||||
},
|
||||
retrieveSeriesMetadata: async ({ StudyInstanceUID } = {}) => {
|
||||
// TODO: Rename this it makes no sense at all
|
||||
retrieveSeriesMetadata: async ({
|
||||
StudyInstanceUID,
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction,
|
||||
madeInClient = false,
|
||||
} = {}) => {
|
||||
if (!StudyInstanceUID) {
|
||||
throw new Error(
|
||||
'Unable to query for SeriesMetadata without StudyInstanceUID'
|
||||
@@ -191,21 +209,34 @@ function createDicomWebApi(dicomWebConfig) {
|
||||
} = await retrieveStudyMetadata(
|
||||
wadoDicomWebClient,
|
||||
StudyInstanceUID,
|
||||
enableStudyLazyLoad
|
||||
enableStudyLazyLoad,
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction
|
||||
);
|
||||
|
||||
// Async load series, store as retrieved
|
||||
function storeInstances(instances) {
|
||||
const naturalizedInstances = instances.map(naturalizeDataset);
|
||||
|
||||
DicomMetadataStore.addInstances(naturalizedInstances);
|
||||
DicomMetadataStore.addInstances(naturalizedInstances, madeInClient);
|
||||
}
|
||||
|
||||
DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata);
|
||||
function setSuccessFlag() {
|
||||
const study = DicomMetadataStore.getStudy(
|
||||
StudyInstanceUID,
|
||||
madeInClient
|
||||
);
|
||||
study.isLoaded = true;
|
||||
}
|
||||
|
||||
seriesPromises.forEach(async seriesPromise => {
|
||||
DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata, madeInClient);
|
||||
|
||||
const numberOfSeries = seriesPromises.length;
|
||||
seriesPromises.forEach(async (seriesPromise, index) => {
|
||||
const instances = await seriesPromise;
|
||||
storeInstances(instances);
|
||||
if (index === numberOfSeries - 1) setSuccessFlag();
|
||||
});
|
||||
},
|
||||
deleteStudyMetadataPromise,
|
||||
@@ -222,21 +253,28 @@ function createDicomWebApi(dicomWebConfig) {
|
||||
|
||||
if (NumberOfFrames > 1) {
|
||||
for (let i = 0; i < NumberOfFrames; i++) {
|
||||
const imageId = getImageId({
|
||||
const imageId = this.getImageIdsForInstance({
|
||||
instance,
|
||||
frame: i,
|
||||
config: dicomWebConfig,
|
||||
});
|
||||
imageIds.push(imageId);
|
||||
}
|
||||
} else {
|
||||
const imageId = getImageId({ instance, config: dicomWebConfig });
|
||||
const imageId = this.getImageIdsForInstance({ instance });
|
||||
imageIds.push(imageId);
|
||||
}
|
||||
});
|
||||
|
||||
return imageIds;
|
||||
},
|
||||
getImageIdsForInstance({ instance, frame }) {
|
||||
const imageIds = getImageId({
|
||||
instance,
|
||||
frame,
|
||||
config: dicomWebConfig,
|
||||
});
|
||||
return imageIds;
|
||||
},
|
||||
};
|
||||
|
||||
if (supportsReject) {
|
||||
|
||||
@@ -19,7 +19,9 @@ export function retrieveStudyMetadata(
|
||||
dicomWebClient,
|
||||
StudyInstanceUID,
|
||||
enableStudyLazyLoad,
|
||||
filters
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction
|
||||
) {
|
||||
// @TODO: Whenever a study metadata request has failed, its related promise will be rejected once and for all
|
||||
// and further requests for that metadata will always fail. On failure, we probably need to remove the
|
||||
@@ -47,7 +49,9 @@ export function retrieveStudyMetadata(
|
||||
dicomWebClient,
|
||||
StudyInstanceUID,
|
||||
enableStudyLazyLoad,
|
||||
filters
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction
|
||||
).then(function(data) {
|
||||
resolve(data);
|
||||
}, reject);
|
||||
|
||||
@@ -34,6 +34,10 @@ export default function getImageId({
|
||||
return;
|
||||
}
|
||||
|
||||
if (instance.url) {
|
||||
return instance.url;
|
||||
}
|
||||
|
||||
const renderingAttr = thumbnail ? 'thumbnailRendering' : 'imageRendering';
|
||||
|
||||
if (!config[renderingAttr] || config[renderingAttr] === 'wadouri') {
|
||||
|
||||
@@ -43,9 +43,11 @@ const sortingCriteria = {
|
||||
*/
|
||||
const sortStudySeries = (
|
||||
series,
|
||||
seriesSortingCriteria = seriesSortCriteria.default
|
||||
seriesSortingCriteria = seriesSortCriteria.default,
|
||||
sortFunction
|
||||
) => {
|
||||
return series.sort(seriesSortingCriteria);
|
||||
if (typeof sortFunction === 'function') return sortFunction(series);
|
||||
else return series.sort(seriesSortingCriteria);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,9 @@ async function RetrieveMetadata(
|
||||
dicomWebClient,
|
||||
studyInstanceUid,
|
||||
enableStudyLazyLoad,
|
||||
filters = {}
|
||||
filters = {},
|
||||
sortCriteria,
|
||||
sortFunction
|
||||
) {
|
||||
// const RetrieveMetadataLoader =
|
||||
// enableStudyLazyLoad !== false
|
||||
@@ -27,7 +29,9 @@ async function RetrieveMetadata(
|
||||
const retrieveMetadataLoader = new RetrieveMetadataLoader(
|
||||
dicomWebClient,
|
||||
studyInstanceUid,
|
||||
filters
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction
|
||||
);
|
||||
const { preLoadData, promises } = await retrieveMetadataLoader.execLoad();
|
||||
|
||||
|
||||
@@ -12,11 +12,20 @@ export default class RetrieveMetadataLoader {
|
||||
* @param {Array} studyInstanceUID Study instance ui to be retrieved
|
||||
* @param {Object} [filters] - Object containing filters to be applied on retrieve metadata process
|
||||
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
|
||||
* @param {Function} [sortSeries] - Custom sort function for series
|
||||
*/
|
||||
constructor(client, studyInstanceUID, filters = {}) {
|
||||
constructor(
|
||||
client,
|
||||
studyInstanceUID,
|
||||
filters = {},
|
||||
sortCriteria,
|
||||
sortFunction
|
||||
) {
|
||||
this.client = client;
|
||||
this.studyInstanceUID = studyInstanceUID;
|
||||
this.filters = filters;
|
||||
this.sortCriteria = sortCriteria;
|
||||
this.sortFunction = sortFunction;
|
||||
}
|
||||
|
||||
async execLoad() {
|
||||
|
||||
@@ -2,6 +2,7 @@ import dcmjs from 'dcmjs';
|
||||
import { sortStudySeries, sortingCriteria } from '../utils/sortStudy';
|
||||
import RetrieveMetadataLoader from './retrieveMetadataLoader';
|
||||
|
||||
|
||||
/**
|
||||
* Creates an immutable series loader object which loads each series sequentially using the iterator interface
|
||||
* @param {DICOMWebClient} dicomWebClient The DICOMWebClient instance to be used for series load
|
||||
@@ -62,13 +63,17 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader
|
||||
async preLoad() {
|
||||
const preLoaders = this.getPreLoaders();
|
||||
const result = await this.runLoaders(preLoaders);
|
||||
const sortCriteria = this.sortCriteria;
|
||||
const sortFunction = this.sortFunction;
|
||||
|
||||
const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary;
|
||||
const naturalized = result.map(naturalizeDataset);
|
||||
|
||||
return sortStudySeries(
|
||||
naturalized,
|
||||
sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria
|
||||
sortCriteria ||
|
||||
sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria,
|
||||
sortFunction
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,12 +21,13 @@ export default function PanelMeasurementTable({
|
||||
|
||||
// ~~ Subscription
|
||||
const added = MeasurementService.EVENTS.MEASUREMENT_ADDED;
|
||||
const addedRaw = MeasurementService.EVENTS.RAW_MEASUREMENT_ADDED;
|
||||
const updated = MeasurementService.EVENTS.MEASUREMENT_UPDATED;
|
||||
const removed = MeasurementService.EVENTS.MEASUREMENT_REMOVED;
|
||||
const cleared = MeasurementService.EVENTS.MEASUREMENTS_CLEARED;
|
||||
const subscriptions = [];
|
||||
|
||||
[added, updated, removed, cleared].forEach(evt => {
|
||||
[added, addedRaw, updated, removed, cleared].forEach(evt => {
|
||||
subscriptions.push(
|
||||
MeasurementService.subscribe(evt, () => {
|
||||
debouncedSetDisplayMeasurements(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { StudyBrowser, useImageViewer } from '@ohif/ui';
|
||||
import { utils } from '@ohif/core';
|
||||
@@ -19,7 +19,7 @@ function PanelStudyBrowser({
|
||||
// Normally you nest the components so the tree isn't so deep, and the data
|
||||
// doesn't have to have such an intense shape. This works well enough for now.
|
||||
// Tabs --> Studies --> DisplaySets --> Thumbnails
|
||||
const [{ StudyInstanceUIDs }, dispatch] = useImageViewer();
|
||||
const { StudyInstanceUIDs } = useImageViewer();
|
||||
const [activeTabName, setActiveTabName] = useState('primary');
|
||||
const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([
|
||||
...StudyInstanceUIDs,
|
||||
@@ -27,6 +27,7 @@ function PanelStudyBrowser({
|
||||
const [studyDisplayList, setStudyDisplayList] = useState([]);
|
||||
const [displaySets, setDisplaySets] = useState([]);
|
||||
const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({});
|
||||
const isMounted = useRef(true);
|
||||
|
||||
// ~~ studyDisplayList
|
||||
useEffect(() => {
|
||||
@@ -47,8 +48,9 @@ function PanelStudyBrowser({
|
||||
// displaySets: []
|
||||
};
|
||||
});
|
||||
|
||||
setStudyDisplayList(actuallyMappedStudies);
|
||||
if (isMounted.current) {
|
||||
setStudyDisplayList(actuallyMappedStudies);
|
||||
}
|
||||
}
|
||||
|
||||
StudyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid));
|
||||
@@ -72,11 +74,16 @@ function PanelStudyBrowser({
|
||||
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
|
||||
imageId
|
||||
);
|
||||
setThumbnailImageSrcMap(prevState => {
|
||||
return { ...prevState, ...newImageSrcEntry };
|
||||
});
|
||||
if (isMounted.current) {
|
||||
setThumbnailImageSrcMap(prevState => {
|
||||
return { ...prevState, ...newImageSrcEntry };
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -112,11 +119,14 @@ function PanelStudyBrowser({
|
||||
if (imageId) {
|
||||
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
|
||||
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
|
||||
imageId
|
||||
imageId,
|
||||
dSet.initialViewport
|
||||
);
|
||||
setThumbnailImageSrcMap(prevState => {
|
||||
return { ...prevState, ...newImageSrcEntry };
|
||||
});
|
||||
if (isMounted.current) {
|
||||
setThumbnailImageSrcMap(prevState => {
|
||||
return { ...prevState, ...newImageSrcEntry };
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ async function getStudiesForPatientByStudyInstanceUID(
|
||||
dataSource,
|
||||
StudyInstanceUID
|
||||
) {
|
||||
if (StudyInstanceUID === undefined) return;
|
||||
// TODO: The `DicomMetadataStore` should short-circuit both of these requests
|
||||
// Data _could_ be here from route query, or if using JSON data source
|
||||
// We could also force this to "await" these values being available in the DICOMStore?
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
useModal,
|
||||
} from '@ohif/ui';
|
||||
|
||||
import { useAppConfig } from '@state';
|
||||
|
||||
function Toolbar({ servicesManager }) {
|
||||
const { ToolBarService } = servicesManager.services;
|
||||
const [toolbarButtons, setToolbarButtons] = useState([]);
|
||||
@@ -45,7 +47,10 @@ function Toolbar({ servicesManager }) {
|
||||
// isActive if:
|
||||
// - id is primary?
|
||||
// - id is in list of "toggled on"?
|
||||
|
||||
let isActive;
|
||||
if (componentProps.type === 'toggle') {
|
||||
isActive = buttonState.toggles[id];
|
||||
}
|
||||
// Also need... to filter list for splitButton, and set primary based on most recently clicked
|
||||
// Also need to kill the radioGroup button's magic logic
|
||||
// Everything should be reactive off these props, so commands can inform ToolbarService
|
||||
@@ -58,6 +63,7 @@ function Toolbar({ servicesManager }) {
|
||||
id={id}
|
||||
{...componentProps}
|
||||
bState={buttonState}
|
||||
isActive={isActive}
|
||||
onInteraction={args => ToolBarService.recordInteraction(args)}
|
||||
/>
|
||||
);
|
||||
@@ -78,6 +84,8 @@ function ViewerLayout({
|
||||
viewports,
|
||||
ViewportGridComp,
|
||||
}) {
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { show, hide } = useModal();
|
||||
|
||||
@@ -154,7 +162,7 @@ function ViewerLayout({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header menuOptions={menuOptions}>
|
||||
<Header menuOptions={menuOptions} WhiteLabeling={appConfig.whiteLabeling} >
|
||||
<ErrorBoundary context="Primary Toolbar">
|
||||
<div className="relative flex justify-center">
|
||||
<Toolbar servicesManager={servicesManager} />
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||
const { MeasurementService, ViewportGridService, ToolBarService, CineService } = servicesManager.services;
|
||||
const {
|
||||
MeasurementService,
|
||||
ViewportGridService,
|
||||
ToolBarService,
|
||||
HangingProtocolService,
|
||||
CineService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const actions = {
|
||||
clearMeasurements: () => {
|
||||
@@ -10,7 +16,16 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||
const { isCineEnabled } = CineService.getState();
|
||||
CineService.setIsCineEnabled(!isCineEnabled);
|
||||
ToolBarService.setButton('Cine', { props: { isActive: !isCineEnabled } });
|
||||
viewports.forEach((_, index) => CineService.setCine({ id: index, isPlaying: false }));
|
||||
viewports.forEach((_, index) =>
|
||||
CineService.setCine({ id: index, isPlaying: false })
|
||||
);
|
||||
},
|
||||
nextStage: () => {
|
||||
// next stage in hanging protocols
|
||||
HangingProtocolService.nextProtocolStage();
|
||||
},
|
||||
previousStage: () => {
|
||||
HangingProtocolService.previousProtocolStage();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,6 +40,16 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
nextStage: {
|
||||
commandFn: actions.nextStage,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
previousStage: {
|
||||
commandFn: actions.previousStage,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// TODO: Use existing DICOMWeb configuration (previously, appConfig, to configure instance)
|
||||
|
||||
import { createDicomWebApi } from './DicomWebDataSource/index.js';
|
||||
import { createDicomJSONApi } from './DicomJSONDataSource/index.js';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -14,6 +15,11 @@ function getDataSourcesModule() {
|
||||
type: 'webApi',
|
||||
createDataSource: createDicomWebApi,
|
||||
},
|
||||
{
|
||||
name: 'dicomjson',
|
||||
type: 'jsonApi',
|
||||
createDataSource: createDicomJSONApi,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
const hangingProtocolName = 'petCT';
|
||||
|
||||
const petCTProtocol = {
|
||||
id: 'PET/CT',
|
||||
locked: true,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'PET/CT',
|
||||
createdDate: '2021-02-23T18:32:42.849Z',
|
||||
modifiedDate: '2021-02-23T18:32:42.849Z',
|
||||
availableTo: {},
|
||||
editableBy: {},
|
||||
protocolMatchingRules: [
|
||||
{
|
||||
id: 'wauZK2QNEfDPwcAQo',
|
||||
weight: 1,
|
||||
attribute: 'StudyInstanceUID',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: '1.3.6.1.4.1.25403.345050719074.3824.20170125112931.11',
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
stages: [
|
||||
{
|
||||
id: 'hYbmMy3b7pz7GLiaT',
|
||||
name: 'oneByTwo',
|
||||
viewportStructure: {
|
||||
type: 'grid',
|
||||
properties: {
|
||||
rows: 2,
|
||||
columns: 2,
|
||||
},
|
||||
},
|
||||
viewports: [
|
||||
{
|
||||
viewportSettings: [
|
||||
{
|
||||
options: {
|
||||
voi: {
|
||||
windowWidth: 500,
|
||||
windowCenter: 500,
|
||||
},
|
||||
},
|
||||
commandName: '',
|
||||
// Type can be viewport or prop
|
||||
// viewport: It is most suited for settings that
|
||||
// should be applied before the first render
|
||||
// prop: It is the type of command that can be applied
|
||||
// after the image render, such as tool activations.
|
||||
type: 'viewport',
|
||||
},
|
||||
],
|
||||
imageMatchingRules: [],
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
id: 'vSjk7NCYjtdS3XZAw',
|
||||
weight: 1,
|
||||
attribute: 'SeriesNumber',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: "4",
|
||||
},
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
studyMatchingRules: [],
|
||||
},
|
||||
{
|
||||
viewportSettings: [
|
||||
{
|
||||
options: {
|
||||
invert: true,
|
||||
},
|
||||
commandName: '',
|
||||
type: 'viewport',
|
||||
},
|
||||
],
|
||||
imageMatchingRules: [
|
||||
|
||||
],
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
id: 'GPEYqFLv2dwzCM322',
|
||||
weight: 1,
|
||||
attribute: 'SeriesNumber',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: "1",
|
||||
},
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
studyMatchingRules: [
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportSettings: [
|
||||
{
|
||||
options: {
|
||||
invert: true,
|
||||
},
|
||||
commandName: '',
|
||||
type: 'viewport',
|
||||
},
|
||||
],
|
||||
imageMatchingRules: [
|
||||
|
||||
],
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
id: 'GPEYqFLv2dwzCM322',
|
||||
weight: 1,
|
||||
attribute: 'SeriesNumber',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: "6",
|
||||
},
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
studyMatchingRules: [
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportSettings: [
|
||||
{
|
||||
options: {
|
||||
invert: true,
|
||||
},
|
||||
commandName: '',
|
||||
type: 'viewport',
|
||||
},
|
||||
],
|
||||
imageMatchingRules: [
|
||||
|
||||
],
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
id: 'GPEYqFLv2dwzCM322',
|
||||
weight: 1,
|
||||
attribute: 'SeriesDescription',
|
||||
constraint: {
|
||||
contains: {
|
||||
value: "Corrected",
|
||||
},
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'GPEYqFLv2dwzCM322',
|
||||
weight: 2,
|
||||
attribute: 'SeriesDescription',
|
||||
constraint: {
|
||||
doesNotContain: {
|
||||
value: "Uncorrected",
|
||||
},
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
studyMatchingRules: [
|
||||
|
||||
],
|
||||
},
|
||||
],
|
||||
createdDate: '2021-02-23T18:32:42.850Z',
|
||||
},
|
||||
],
|
||||
numberOfPriorsReferenced: 0,
|
||||
};
|
||||
|
||||
function getHangingProtocolModule() {
|
||||
return [
|
||||
{
|
||||
name: hangingProtocolName,
|
||||
protocols: [petCTProtocol],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default getHangingProtocolModule;
|
||||
@@ -3,6 +3,7 @@ import getDataSourcesModule from './getDataSourcesModule.js';
|
||||
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
|
||||
import getPanelModule from './getPanelModule.js';
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule.js';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule.js';
|
||||
import getToolbarModule from './getToolbarModule.js';
|
||||
import commandsModule from './commandsModule';
|
||||
import id from './id';
|
||||
@@ -14,6 +15,7 @@ export default {
|
||||
id,
|
||||
getContextModule,
|
||||
getDataSourcesModule,
|
||||
getHangingProtocolModule,
|
||||
getLayoutTemplateModule,
|
||||
getPanelModule,
|
||||
getSopClassHandlerModule,
|
||||
|
||||
@@ -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",
|
||||
@@ -40,7 +40,7 @@
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.11.0",
|
||||
"react-cornerstone-viewport": "4.0.2"
|
||||
"react-cornerstone-viewport": "4.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.7.6",
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"dcmjs": "0.16.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.13.1",
|
||||
"react-cornerstone-viewport": "^4.0.4",
|
||||
"react-cornerstone-viewport": "^4.0.5",
|
||||
"react-dom": "^16.13.1",
|
||||
"webpack": "^4.0.0",
|
||||
"webpack-merge": "^5.7.3"
|
||||
|
||||
+1
-1
@@ -12,7 +12,6 @@ import promptTrackNewSeries from './promptTrackNewSeries';
|
||||
import promptTrackNewStudy from './promptTrackNewStudy';
|
||||
import promptSaveReport from './promptSaveReport';
|
||||
import promptHydrateStructuredReport from './promptHydrateStructuredReport';
|
||||
import hydrateStructuredReport from './_hydrateStructuredReport.js';
|
||||
|
||||
const TrackedMeasurementsContext = React.createContext();
|
||||
TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext';
|
||||
@@ -99,6 +98,7 @@ function TrackedMeasurementsContextProvider(
|
||||
}),
|
||||
promptHydrateStructuredReport: promptHydrateStructuredReport.bind(null, {
|
||||
servicesManager,
|
||||
extensionManager
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
+16
-26
@@ -1,7 +1,7 @@
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import OHIF, { DicomMetadataStore } from '@ohif/core';
|
||||
import getLabelFromDCMJSImportedToolData from './utils/getLabelFromDCMJSImportedToolData';
|
||||
import getToolStateToCornerstoneMeasurementSchema from './getToolStateToCornerstoneMeasurementSchema';
|
||||
import getCornerstoneToolStateToMeasurementSchema from './getCornerstoneToolStateToMeasurementSchema';
|
||||
import { adapters } from 'dcmjs';
|
||||
|
||||
const { guid } = OHIF.utils;
|
||||
@@ -15,6 +15,7 @@ export default function _hydrateStructuredReport(
|
||||
{ servicesManager, extensionManager },
|
||||
displaySetInstanceUID
|
||||
) {
|
||||
const dataSource = extensionManager.getActiveDataSource()[0];
|
||||
const { MeasurementService, DisplaySetService } = servicesManager.services;
|
||||
|
||||
const displaySet = DisplaySetService.getDisplaySetByUID(
|
||||
@@ -114,14 +115,23 @@ export default function _hydrateStructuredReport(
|
||||
|
||||
data.id = guid();
|
||||
|
||||
_addToolDataToCornerstoneTools(data, toolType, imageId);
|
||||
const instance = cornerstone.metaData.get('instance', imageId);
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = instance;
|
||||
|
||||
// Let the measurement service know we added to toolState
|
||||
const toMeasurementSchema = getToolStateToCornerstoneMeasurementSchema(
|
||||
const toMeasurementSchema = getCornerstoneToolStateToMeasurementSchema(
|
||||
toolType,
|
||||
MeasurementService,
|
||||
DisplaySetService,
|
||||
imageId
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID
|
||||
);
|
||||
|
||||
const source = MeasurementService.getSource('CornerstoneTools', '4');
|
||||
@@ -132,7 +142,8 @@ export default function _hydrateStructuredReport(
|
||||
source,
|
||||
toolType,
|
||||
data,
|
||||
toMeasurementSchema
|
||||
toMeasurementSchema,
|
||||
dataSource
|
||||
);
|
||||
|
||||
if (!imageIds.includes(imageId)) {
|
||||
@@ -148,24 +159,3 @@ export default function _hydrateStructuredReport(
|
||||
SeriesInstanceUIDs,
|
||||
};
|
||||
}
|
||||
|
||||
function _addToolDataToCornerstoneTools(data, toolType, imageId) {
|
||||
const toolState = 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);
|
||||
}
|
||||
+38
-38
@@ -1,8 +1,11 @@
|
||||
export default function getToolStateToCornerstoneMeasurementSchema(
|
||||
export default function getCornerstoneToolStateToMeasurementSchema(
|
||||
toolType,
|
||||
MeasurementService,
|
||||
DisplaySetService,
|
||||
imageId
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID
|
||||
) {
|
||||
const _getValueTypeFromToolType = toolType => {
|
||||
const {
|
||||
@@ -31,7 +34,10 @@ export default function getToolStateToCornerstoneMeasurementSchema(
|
||||
return measurementData =>
|
||||
Length(
|
||||
measurementData,
|
||||
imageId,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
DisplaySetService,
|
||||
_getValueTypeFromToolType
|
||||
);
|
||||
@@ -39,7 +45,10 @@ export default function getToolStateToCornerstoneMeasurementSchema(
|
||||
return measurementData =>
|
||||
Bidirectional(
|
||||
measurementData,
|
||||
imageId,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
DisplaySetService,
|
||||
_getValueTypeFromToolType
|
||||
);
|
||||
@@ -47,7 +56,10 @@ export default function getToolStateToCornerstoneMeasurementSchema(
|
||||
return measurementData =>
|
||||
EllipticalRoi(
|
||||
measurementData,
|
||||
imageId,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
DisplaySetService,
|
||||
_getValueTypeFromToolType
|
||||
);
|
||||
@@ -55,7 +67,10 @@ export default function getToolStateToCornerstoneMeasurementSchema(
|
||||
return measurementData =>
|
||||
ArrowAnnotate(
|
||||
measurementData,
|
||||
imageId,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
DisplaySetService,
|
||||
_getValueTypeFromToolType
|
||||
);
|
||||
@@ -64,18 +79,14 @@ export default function getToolStateToCornerstoneMeasurementSchema(
|
||||
|
||||
function Length(
|
||||
measurementData,
|
||||
imageId,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
DisplaySetService,
|
||||
_getValueTypeFromToolType
|
||||
) {
|
||||
const tool = measurementData.toolType || measurementData.toolName;
|
||||
const instance = cornerstone.metaData.get('instance', imageId);
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = instance;
|
||||
|
||||
const displaySetInstanceUID = _getDisplaySetInstanceUID(
|
||||
DisplaySetService,
|
||||
@@ -113,18 +124,15 @@ function Length(
|
||||
|
||||
function Bidirectional(
|
||||
measurementData,
|
||||
imageId,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
DisplaySetService,
|
||||
_getValueTypeFromToolType
|
||||
) {
|
||||
const tool = measurementData.toolType || measurementData.toolName;
|
||||
const instance = cornerstone.metaData.get('instance', imageId);
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = instance;
|
||||
|
||||
const displaySetInstanceUID = _getDisplaySetInstanceUID(
|
||||
DisplaySetService,
|
||||
SeriesInstanceUID,
|
||||
@@ -155,18 +163,14 @@ function Bidirectional(
|
||||
|
||||
function EllipticalRoi(
|
||||
measurementData,
|
||||
imageId,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
DisplaySetService,
|
||||
_getValueTypeFromToolType
|
||||
) {
|
||||
const tool = measurementData.toolType || measurementData.toolName;
|
||||
const instance = cornerstone.metaData.get('instance', imageId);
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = instance;
|
||||
|
||||
const displaySetInstanceUID = _getDisplaySetInstanceUID(
|
||||
DisplaySetService,
|
||||
@@ -222,18 +226,14 @@ function EllipticalRoi(
|
||||
|
||||
function ArrowAnnotate(
|
||||
measurementData,
|
||||
imageId,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
DisplaySetService,
|
||||
_getValueTypeFromToolType
|
||||
) {
|
||||
const tool = measurementData.toolType || measurementData.toolName;
|
||||
const instance = cornerstone.metaData.get('instance', imageId);
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = instance;
|
||||
|
||||
const displaySetInstanceUID = _getDisplaySetInstanceUID(
|
||||
DisplaySetService,
|
||||
+1
-1
@@ -6,7 +6,7 @@ const RESPONSE = {
|
||||
SET_STUDY_AND_SERIES: 3,
|
||||
};
|
||||
|
||||
function promptUser({ servicesManager }, ctx, evt) {
|
||||
function promptUser({ servicesManager, extensionManager }, ctx, evt) {
|
||||
const { UIViewportDialogService } = servicesManager.services;
|
||||
const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt;
|
||||
|
||||
|
||||
@@ -12,12 +12,13 @@ const OHIFCornerstoneViewport = props => {
|
||||
);
|
||||
};
|
||||
|
||||
function getViewportModule({ servicesManager, commandsManager }) {
|
||||
function getViewportModule({ servicesManager, commandsManager, extensionManager }) {
|
||||
const ExtendedOHIFCornerstoneTrackingViewport = props => {
|
||||
return (
|
||||
<OHIFCornerstoneViewport
|
||||
servicesManager={servicesManager}
|
||||
commandsManager={commandsManager}
|
||||
extensionManager={extensionManager}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -114,15 +114,16 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
// Are we exposing the right API for measurementService?
|
||||
// This watches for ALL MeasurementService changes. It updates a timestamp,
|
||||
// which is debounced. After a brief period of inactivity, this triggers
|
||||
// a re-render where we grab up-to-date measurements.
|
||||
// a re-render where we grab up-to-date measurements
|
||||
useEffect(() => {
|
||||
const added = MeasurementService.EVENTS.MEASUREMENT_ADDED;
|
||||
const addedRaw = MeasurementService.EVENTS.RAW_MEASUREMENT_ADDED;
|
||||
const updated = MeasurementService.EVENTS.MEASUREMENT_UPDATED;
|
||||
const removed = MeasurementService.EVENTS.MEASUREMENT_REMOVED;
|
||||
const cleared = MeasurementService.EVENTS.MEASUREMENTS_CLEARED;
|
||||
const subscriptions = [];
|
||||
|
||||
[added, updated, removed, cleared].forEach(evt => {
|
||||
[added, addedRaw, updated, removed, cleared].forEach(evt => {
|
||||
subscriptions.push(
|
||||
MeasurementService.subscribe(evt, () => {
|
||||
setMeasurementsUpdated(Date.now().toString());
|
||||
|
||||
+27
-20
@@ -28,7 +28,7 @@ function PanelStudyBrowserTracking({
|
||||
// Normally you nest the components so the tree isn't so deep, and the data
|
||||
// doesn't have to have such an intense shape. This works well enough for now.
|
||||
// Tabs --> Studies --> DisplaySets --> Thumbnails
|
||||
const [{ StudyInstanceUIDs }, dispatchImageViewer] = useImageViewer();
|
||||
const { StudyInstanceUIDs } = useImageViewer();
|
||||
const [
|
||||
{ activeViewportIndex, viewports, numCols, numRows },
|
||||
viewportGridService,
|
||||
@@ -58,27 +58,34 @@ function PanelStudyBrowserTracking({
|
||||
|
||||
const isSingleViewport = numCols === 1 && numRows === 1;
|
||||
|
||||
// TODO: Should this be somewhere else? Feels more like a mode "lifecycle" setup/destroy?
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = MeasurementService.subscribe(
|
||||
MeasurementService.EVENTS.MEASUREMENT_ADDED,
|
||||
({ source, measurement }) => {
|
||||
const {
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
referenceStudyUID: StudyInstanceUID,
|
||||
} = measurement;
|
||||
const added = MeasurementService.EVENTS.MEASUREMENT_ADDED;
|
||||
const addedRaw = MeasurementService.EVENTS.RAW_MEASUREMENT_ADDED;
|
||||
const subscriptions = [];
|
||||
|
||||
sendTrackedMeasurementsEvent('SET_DIRTY', { SeriesInstanceUID });
|
||||
sendTrackedMeasurementsEvent('TRACK_SERIES', {
|
||||
viewportIndex: activeViewportIndex,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
});
|
||||
}
|
||||
);
|
||||
[added, addedRaw].forEach(evt => {
|
||||
subscriptions.push(
|
||||
MeasurementService.subscribe(evt, ({ source, measurement }) => {
|
||||
const {
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
referenceStudyUID: StudyInstanceUID,
|
||||
} = measurement;
|
||||
|
||||
return unsubscribe;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
sendTrackedMeasurementsEvent('SET_DIRTY', { SeriesInstanceUID });
|
||||
sendTrackedMeasurementsEvent('TRACK_SERIES', {
|
||||
viewportIndex: activeViewportIndex,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
});
|
||||
}).unsubscribe
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach(unsub => {
|
||||
unsub();
|
||||
});
|
||||
};
|
||||
}, [MeasurementService, activeViewportIndex, sendTrackedMeasurementsEvent]);
|
||||
|
||||
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
|
||||
@@ -89,7 +96,6 @@ function PanelStudyBrowserTracking({
|
||||
async function fetchStudiesForPatient(StudyInstanceUID) {
|
||||
const qidoStudiesForPatient =
|
||||
(await getStudiesForPatientByStudyInstanceUID(StudyInstanceUID)) || [];
|
||||
|
||||
// TODO: This should be "naturalized DICOM JSON" studies
|
||||
const mappedStudies = _mapDataSourceStudies(qidoStudiesForPatient);
|
||||
const actuallyMappedStudies = mappedStudies.map(qidoStudy => {
|
||||
@@ -182,6 +188,7 @@ function PanelStudyBrowserTracking({
|
||||
|
||||
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
|
||||
const imageId = imageIds[Math.floor(imageIds.length / 2)];
|
||||
|
||||
// TODO: Is it okay that imageIds are not returned here for SR displaysets?
|
||||
if (imageId) {
|
||||
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
|
||||
|
||||
+1
@@ -2,6 +2,7 @@ async function getStudiesForPatientByStudyInstanceUID(
|
||||
dataSource,
|
||||
StudyInstanceUID
|
||||
) {
|
||||
if (StudyInstanceUID === undefined) return;
|
||||
// TODO: The `DicomMetadataStore` should short-circuit both of these requests
|
||||
// Data _could_ be here from route query, or if using JSON data source
|
||||
// We could also force this to "await" these values being available in the DICOMStore?
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { useEffect, useState } 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, { utils } from '@ohif/core';
|
||||
import {
|
||||
Notification,
|
||||
@@ -12,14 +11,10 @@ import {
|
||||
useViewportDialog,
|
||||
} from '@ohif/ui';
|
||||
import { useTrackedMeasurements } from './../getContextModule';
|
||||
|
||||
import ViewportOverlay from './ViewportOverlay';
|
||||
import ViewportLoadingIndicator from './ViewportLoadingIndicator';
|
||||
import setCornerstoneMeasurementActive from '../_shared/setCornerstoneMeasurementActive';
|
||||
import setActiveAndPassiveToolsForElement from '../_shared/setActiveAndPassiveToolsForElement';
|
||||
import getTools from '../_shared/getTools';
|
||||
|
||||
const scrollToIndex = cornerstoneTools.importInternal('util/scrollToIndex');
|
||||
const { formatDate } = utils;
|
||||
|
||||
// TODO -> Get this list from the list of tracked measurements.
|
||||
@@ -40,19 +35,19 @@ const BaseAnnotationTool = cornerstoneTools.importInternal(
|
||||
|
||||
const { StackManager } = OHIF.utils;
|
||||
|
||||
function TrackedCornerstoneViewport({
|
||||
children,
|
||||
dataSource,
|
||||
displaySet,
|
||||
viewportIndex,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
}) {
|
||||
function TrackedCornerstoneViewport(props) {
|
||||
const {
|
||||
ToolBarService,
|
||||
DisplaySetService,
|
||||
MeasurementService,
|
||||
} = servicesManager.services;
|
||||
children,
|
||||
dataSource,
|
||||
displaySet,
|
||||
viewportIndex,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
commandsManager,
|
||||
} = props;
|
||||
|
||||
const { ToolBarService } = servicesManager.services;
|
||||
|
||||
const [trackedMeasurements] = useTrackedMeasurements();
|
||||
const [
|
||||
{ activeViewportIndex, viewports },
|
||||
@@ -60,73 +55,9 @@ function TrackedCornerstoneViewport({
|
||||
] = useViewportGrid();
|
||||
const [{ isCineEnabled, cines }, cineService] = useCine();
|
||||
const [viewportDialogState, viewportDialogApi] = useViewportDialog();
|
||||
const [viewportData, setViewportData] = useState(null);
|
||||
const [element, setElement] = useState(null);
|
||||
const [isTracked, setIsTracked] = useState(false);
|
||||
const [trackedMeasurementId, setTrackedMeasurementId] = useState(null);
|
||||
|
||||
// TODO: Still needed? Better way than import `OHIF` and destructure?
|
||||
// Why is this managed by `core`?
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
StackManager.clearStacks();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
cineService.setCine({ id: viewportIndex });
|
||||
}, [viewportIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
|
||||
MeasurementService,
|
||||
DisplaySetService,
|
||||
element,
|
||||
viewportIndex,
|
||||
displaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
_checkForCachedJumpToMeasurementEvents(
|
||||
MeasurementService,
|
||||
DisplaySetService,
|
||||
element,
|
||||
viewportIndex,
|
||||
displaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromJumpToMeasurementEvents();
|
||||
};
|
||||
}, [element, displaySet]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
const allTools = cornerstoneTools.store.state.tools;
|
||||
const toolsForElement = allTools.filter(tool => tool.element === element);
|
||||
|
||||
toolsForElement.forEach(tool => {
|
||||
if (
|
||||
tool instanceof ArrowAnnotateTool ||
|
||||
tool instanceof BidirectionalTool ||
|
||||
tool instanceof EllipticalRoiTool ||
|
||||
tool instanceof LengthTool
|
||||
) {
|
||||
const configuration = tool.configuration;
|
||||
|
||||
configuration.renderDashed = !isTracked;
|
||||
|
||||
tool.configuration = configuration;
|
||||
}
|
||||
});
|
||||
|
||||
const enabledElement = cornerstone.getEnabledElement(element);
|
||||
|
||||
if (enabledElement.image) {
|
||||
cornerstone.updateImage(element);
|
||||
}
|
||||
}, [isTracked]);
|
||||
const [element, setElement] = useState(null);
|
||||
|
||||
const onElementEnabled = evt => {
|
||||
const eventData = evt.detail;
|
||||
@@ -191,51 +122,33 @@ function TrackedCornerstoneViewport({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
sopClassUids,
|
||||
} = displaySet;
|
||||
|
||||
if (!StudyInstanceUID || !displaySetInstanceUID) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
const allTools = cornerstoneTools.store.state.tools;
|
||||
const toolsForElement = allTools.filter(tool => tool.element === element);
|
||||
|
||||
if (sopClassUids && sopClassUids.length > 1) {
|
||||
console.warn(
|
||||
'More than one SOPClassUID in the same series is not yet supported.'
|
||||
);
|
||||
}
|
||||
toolsForElement.forEach(tool => {
|
||||
if (
|
||||
tool instanceof ArrowAnnotateTool ||
|
||||
tool instanceof BidirectionalTool ||
|
||||
tool instanceof EllipticalRoiTool ||
|
||||
tool instanceof LengthTool
|
||||
) {
|
||||
const configuration = tool.configuration;
|
||||
|
||||
_getViewportData(dataSource, displaySet).then(setViewportData);
|
||||
}, [dataSource, displaySet, viewports, viewportIndex]);
|
||||
configuration.renderDashed = !isTracked;
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
let childrenWithProps = null;
|
||||
|
||||
if (!viewportData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
imageIds,
|
||||
currentImageIdIndex,
|
||||
// If this comes from the instance, would be a better default
|
||||
// `FrameTime` in the instance
|
||||
// frameRate = 0,
|
||||
} = viewportData.stack;
|
||||
|
||||
if (children && children.length) {
|
||||
childrenWithProps = children.map((child, index) => {
|
||||
return (
|
||||
child &&
|
||||
React.cloneElement(child, {
|
||||
viewportIndex,
|
||||
key: index,
|
||||
})
|
||||
);
|
||||
tool.configuration = configuration;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const enabledElement = cornerstone.getEnabledElement(element);
|
||||
|
||||
if (enabledElement.image) {
|
||||
cornerstone.updateImage(element);
|
||||
}
|
||||
}, [isTracked]);
|
||||
|
||||
// We have...
|
||||
// StudyInstanceUid, DisplaySetInstanceUid
|
||||
@@ -313,6 +226,19 @@ function TrackedCornerstoneViewport({
|
||||
);
|
||||
}
|
||||
|
||||
const renderViewport = () => {
|
||||
const { component: Component } = extensionManager.getModuleEntry(
|
||||
'org.ohif.cornerstone.viewportModule.cornerstone'
|
||||
);
|
||||
return (
|
||||
<Component
|
||||
onElementEnabled={onElementEnabled}
|
||||
element={element}
|
||||
{...props}
|
||||
></Component>
|
||||
);
|
||||
};
|
||||
|
||||
const cine = cines[viewportIndex];
|
||||
const isPlaying = (cine && cine.isPlaying) || false;
|
||||
const frameRate = (cine && cine.frameRate) || 24;
|
||||
@@ -341,10 +267,12 @@ function TrackedCornerstoneViewport({
|
||||
patientSex: PatientSex || '',
|
||||
patientAge: PatientAge || '',
|
||||
MRN: PatientID || '',
|
||||
thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '',
|
||||
thickness: SliceThickness
|
||||
? `${parseFloat(SliceThickness).toFixed(2)}mm`
|
||||
: '',
|
||||
spacing:
|
||||
SpacingBetweenSlices !== undefined
|
||||
? `${SpacingBetweenSlices.toFixed(2)}mm`
|
||||
? `${parseFloat(SpacingBetweenSlices).toFixed(2)}mm`
|
||||
: '',
|
||||
scanner: ManufacturerModelName || '',
|
||||
},
|
||||
@@ -362,29 +290,7 @@ function TrackedCornerstoneViewport({
|
||||
/>
|
||||
{/* TODO: Viewport interface to accept stack or layers of content like this? */}
|
||||
<div className="relative flex flex-row w-full h-full overflow-hidden">
|
||||
<CornerstoneViewport
|
||||
onElementEnabled={onElementEnabled}
|
||||
viewportIndex={viewportIndex}
|
||||
imageIds={imageIds}
|
||||
imageIdIndex={currentImageIdIndex}
|
||||
// Sync resize throttle w/ sidepanel animation duration to prevent
|
||||
// seizure inducing strobe blinking effect
|
||||
resizeRefreshRateMs={150}
|
||||
isActive={true} // todo
|
||||
isStackPrefetchEnabled={true} // todo
|
||||
isPlaying={isPlaying}
|
||||
frameRate={frameRate}
|
||||
isOverlayVisible={true}
|
||||
loadingIndicatorComponent={ViewportLoadingIndicator}
|
||||
viewportOverlayComponent={props => {
|
||||
return (
|
||||
<ViewportOverlay
|
||||
{...props}
|
||||
activeTools={ToolBarService.getActiveTools()}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{renderViewport()}
|
||||
<div className="absolute w-full">
|
||||
{viewportDialogState.viewportIndex === viewportIndex && (
|
||||
<Notification
|
||||
@@ -397,7 +303,6 @@ function TrackedCornerstoneViewport({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{childrenWithProps}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -417,54 +322,6 @@ TrackedCornerstoneViewport.defaultProps = {
|
||||
|
||||
const _viewportLabels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
|
||||
|
||||
/**
|
||||
* Obtain the CornerstoneTools Stack for the specified display set.
|
||||
*
|
||||
* @param {Object} displaySet
|
||||
* @param {Object} dataSource
|
||||
* @return {Object} CornerstoneTools Stack
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
// TODO -> disabled double click for now: onDoubleClick={_onDoubleClick}
|
||||
function _onDoubleClick() {
|
||||
const cancelActiveManipulatorsForElement = cornerstoneTools.getModule(
|
||||
'manipulatorState'
|
||||
).setters.cancelActiveManipulatorsForElement;
|
||||
const enabledElements = cornerstoneTools.store.state.enabledElements;
|
||||
enabledElements.forEach(element => {
|
||||
cancelActiveManipulatorsForElement(element);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the viewport data from a datasource and a displayset.
|
||||
*
|
||||
* @param {Object} dataSource
|
||||
* @param {Object} displaySet
|
||||
* @return {Object} viewport data
|
||||
*/
|
||||
|
||||
async function _getViewportData(dataSource, displaySet) {
|
||||
const stack = _getCornerstoneStack(displaySet, dataSource);
|
||||
|
||||
const viewportData = {
|
||||
StudyInstanceUID: displaySet.StudyInstanceUID,
|
||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||
stack,
|
||||
};
|
||||
|
||||
return viewportData;
|
||||
}
|
||||
|
||||
function _getNextMeasurementId(
|
||||
direction,
|
||||
servicesManager,
|
||||
@@ -518,112 +375,4 @@ function _getNextMeasurementId(
|
||||
return newTrackedMeasurementId;
|
||||
}
|
||||
|
||||
function _subscribeToJumpToMeasurementEvents(
|
||||
MeasurementService,
|
||||
DisplaySetService,
|
||||
element,
|
||||
viewportIndex,
|
||||
displaySetInstanceUID
|
||||
) {
|
||||
const { unsubscribe } = MeasurementService.subscribe(
|
||||
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT,
|
||||
({ viewportIndex: jumpToMeasurementViewportIndex, measurement }) => {
|
||||
// check if the correct viewport index.
|
||||
if (viewportIndex !== jumpToMeasurementViewportIndex) {
|
||||
// Event for a different viewport.
|
||||
return;
|
||||
}
|
||||
|
||||
if (measurement.displaySetInstanceUID !== displaySetInstanceUID) {
|
||||
// Not for this displaySet.
|
||||
return;
|
||||
}
|
||||
|
||||
_jumpToMeasurement(
|
||||
measurement,
|
||||
element,
|
||||
viewportIndex,
|
||||
MeasurementService,
|
||||
DisplaySetService
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return unsubscribe;
|
||||
}
|
||||
|
||||
function _checkForCachedJumpToMeasurementEvents(
|
||||
MeasurementService,
|
||||
DisplaySetService,
|
||||
element,
|
||||
viewportIndex,
|
||||
displaySetInstanceUID
|
||||
) {
|
||||
// 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _jumpToMeasurement(
|
||||
measurement,
|
||||
targetElement,
|
||||
viewportIndex,
|
||||
MeasurementService,
|
||||
DisplaySetService
|
||||
) {
|
||||
const { displaySetInstanceUID, SOPInstanceUID } = measurement;
|
||||
|
||||
const referencedDisplaySet = DisplaySetService.getDisplaySetByUID(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
const imageIndex = referencedDisplaySet.images.findIndex(
|
||||
i => i.SOPInstanceUID === SOPInstanceUID
|
||||
);
|
||||
|
||||
setCornerstoneMeasurementActive(measurement);
|
||||
|
||||
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(
|
||||
cornerstone.EVENTS.IMAGE_RENDERED,
|
||||
scrollToHandler
|
||||
);
|
||||
};
|
||||
targetElement.addEventListener(
|
||||
cornerstone.EVENTS.IMAGE_RENDERED,
|
||||
scrollToHandler
|
||||
);
|
||||
|
||||
if (enabledElement.image) {
|
||||
cornerstone.updateImage(targetElement);
|
||||
}
|
||||
|
||||
// Jump to measurement consumed, remove.
|
||||
MeasurementService.removeJumpToMeasurement(viewportIndex);
|
||||
}
|
||||
}
|
||||
|
||||
export default TrackedCornerstoneViewport;
|
||||
Reference in new issue
Block a user